diff --git a/Makefile b/Makefile index 446e5195..953f761d 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,7 @@ -.PHONY: antlr deb test build check cover +.PHONY: antlr deb test build check cover install + + +export CGO_ENABLED=0 test: gotestsum @@ -35,3 +38,6 @@ check: cover: go test -covermode=count -coverprofile=coverage.out -coverpkg=apigear/... ./... go tool cover -func coverage.out + +install: + go install github.com/apigear-io/cli/cmd/apigear diff --git a/pkg/cmd/gen/expert.go b/pkg/cmd/gen/expert.go index b5477c1d..8306fa23 100644 --- a/pkg/cmd/gen/expert.go +++ b/pkg/cmd/gen/expert.go @@ -8,6 +8,7 @@ import ( "github.com/apigear-io/cli/pkg/log" "github.com/apigear-io/cli/pkg/sol" "github.com/apigear-io/cli/pkg/spec" + "gopkg.in/yaml.v3" "github.com/spf13/cobra" ) @@ -25,6 +26,7 @@ type ExpertOptions struct { force bool watch bool templateDir string + meta string } func NewExpertCommand() *cobra.Command { @@ -68,6 +70,7 @@ func NewExpertCommand() *cobra.Command { cmd.Flags().StringSliceVarP(&options.features, "features", "f", []string{"all"}, "features to enable") cmd.Flags().BoolVarP(&options.force, "force", "", false, "force overwrite") cmd.Flags().BoolVarP(&options.watch, "watch", "", false, "watch for changes") + cmd.Flags().StringVarP(&options.meta, "meta", "m", "", "system meta data") Must(cmd.MarkFlagRequired("input")) Must(cmd.MarkFlagRequired("output")) Must(cmd.MarkFlagRequired("template")) @@ -79,9 +82,18 @@ func makeSolution(options *ExpertOptions) *spec.SolutionDoc { if err != nil { log.Fatal().Err(err).Msg("get current working directory") } + var meta map[string]interface{} + if options.meta != "" { + // convert from yaml to map + err := yaml.Unmarshal([]byte(options.meta), &meta) + if err != nil { + log.Fatal().Err(err).Msg("parse meta data") + } + } return &spec.SolutionDoc{ Schema: "apigear.solution/1.0", RootDir: rootDir, + Meta: meta, Layers: []*spec.SolutionLayer{ { Inputs: options.inputs, diff --git a/pkg/helper/map.go b/pkg/helper/map.go new file mode 100644 index 00000000..7268156b --- /dev/null +++ b/pkg/helper/map.go @@ -0,0 +1,11 @@ +package helper + +func MergeMaps(maps ...map[string]interface{}) map[string]interface{} { + result := make(map[string]interface{}) + for _, m := range maps { + for k, v := range m { + result[k] = v + } + } + return result +} diff --git a/pkg/model/system.go b/pkg/model/system.go index 0dc12468..2fc6454e 100644 --- a/pkg/model/system.go +++ b/pkg/model/system.go @@ -1,5 +1,11 @@ package model +import ( + "strings" + + "github.com/apigear-io/cli/pkg/helper" +) + type System struct { NamedNode `json:",inline" yaml:",inline"` Modules []*Module `json:"modules" yaml:"modules"` @@ -82,3 +88,30 @@ func (s *System) ResolveAll() error { } return nil } + +// ApplyMeta applies the meta data to the system +// It looks for a symbols section in the meta data +// and applies the meta data to the corresponding +// module or interface. +func (s *System) ApplyMeta(meta map[string]interface{}) { + if meta != nil { + helper.MergeMaps(s.Meta, meta) + } + data, ok := s.Meta["symbols"].(map[string]interface{}) + if !ok { + return + } + for k, v := range data { + m := s.LookupModule(k) + if m != nil { + m.Meta = v.(map[string]interface{}) + } + kk := strings.Split(k, ".") + mName := strings.Join(kk[:len(kk)-1], ".") + iName := kk[len(kk)-1] + iface := s.LookupInterface(mName, iName) + if iface != nil { + iface.Meta = v.(map[string]interface{}) + } + } +} diff --git a/pkg/sol/helper.go b/pkg/sol/helper.go index 5a14f112..9780fbe7 100644 --- a/pkg/sol/helper.go +++ b/pkg/sol/helper.go @@ -8,7 +8,7 @@ import ( "github.com/apigear-io/cli/pkg/helper" ) -func GetTemplateDir(rootDir string, template string) (string, error) { +func resolveTemplateDir(rootDir string, template string) (string, error) { var templateDir string if helper.IsDir(helper.Join(rootDir, template)) { templateDir = helper.Join(rootDir, template) diff --git a/pkg/sol/task.go b/pkg/sol/task.go index 772cd892..86ef007d 100644 --- a/pkg/sol/task.go +++ b/pkg/sol/task.go @@ -21,6 +21,17 @@ type task struct { sync.Mutex } +type genConfig struct { + name string + templatesDir string + rulesFile string + outputDir string + inputs []string + features []string + force bool + meta map[string]interface{} +} + func newTask(file string, doc *spec.SolutionDoc) (*task, error) { t := &task{ file: file, @@ -48,8 +59,21 @@ func (t *task) run() error { log.Debug().Msgf("run task %s", t.file) // reset deps t.deps = make([]string, 0) + meta := make(map[string]interface{}) + + // read meta file if exists + if t.doc.MetaFile != "" { + metaFile := helper.Join(t.doc.RootDir, t.doc.MetaFile) + err := helper.ReadDocument(metaFile, &meta) + if err != nil { + return err + } + } + cfg := genConfig{} + cfg.meta = meta + for _, layer := range t.doc.Layers { - err := t.processLayer(layer) + err := t.processLayer(layer, cfg) if err != nil { return err } @@ -59,43 +83,48 @@ func (t *task) run() error { // processLayer processes a layer from the solution. // A layer contains information about the inputs, used template and output. -func (t *task) processLayer(layer *spec.SolutionLayer) error { +func (t *task) processLayer(layer *spec.SolutionLayer, cfg genConfig) error { log.Debug().Msgf("process layer %s", layer.Name) rootDir := t.doc.RootDir - // TODO: template can be a dir or a name of a template - var templateDir string - td, err := GetTemplateDir(rootDir, layer.Template) + + td, err := resolveTemplateDir(rootDir, layer.Template) if err != nil { return err } - templateDir = td - var templatesDir = helper.Join(templateDir, "templates") - var rulesFile = helper.Join(templateDir, "rules.yaml") - var outputDir = helper.Join(rootDir, layer.Output) - // add templates dir and rules file as dependency - t.deps = append(t.deps, templatesDir, rulesFile) - var force = layer.Force - name := layer.Name - if name == "" { + + cfg.templatesDir = helper.Join(td, "templates") + cfg.rulesFile = helper.Join(td, "rules.yaml") + // add templates dir and rules file as watcher dependency + t.deps = append(t.deps, cfg.templatesDir, cfg.rulesFile) + + cfg.outputDir = helper.Join(rootDir, layer.Output) + + cfg.name = layer.Name + if layer.Name == "" { // if no layer name, name is the last part of the output directory - name = filepath.Base(outputDir) + cfg.name = filepath.Base(cfg.outputDir) } + if layer.Inputs == nil { return fmt.Errorf("inputs are empty") } - features := layer.Features - if features == nil { - features = []string{"all"} + cfg.inputs = layer.Inputs + + cfg.features = layer.Features + if layer.Features == nil { + cfg.features = []string{"all"} } + cfg.force = layer.Force + + // merge all meta data sources + cfg.meta = helper.MergeMaps(cfg.meta, t.doc.Meta, layer.Meta) - return t.runGenerator(name, layer.Inputs, outputDir, templateDir, features, force) + return t.runGenerator(cfg) } -func (t *task) runGenerator(name string, inputs []string, outputDir string, templateDir string, features []string, force bool) error { - log.Debug().Msgf("run generator %s %v", name, inputs) - var templatesDir = helper.Join(templateDir, "templates") - var rulesFile = helper.Join(templateDir, "rules.yaml") - expanded, err := expandInputs(t.doc.RootDir, inputs) +func (t *task) runGenerator(cfg genConfig) error { + log.Debug().Msgf("run generator %s %v", cfg.name, cfg.inputs) + expanded, err := expandInputs(t.doc.RootDir, cfg.inputs) if err != nil { return err } @@ -106,29 +135,32 @@ func (t *task) runGenerator(name string, inputs []string, outputDir string, temp } t.deps = append(t.deps, expanded...) - system := model.NewSystem(name) + system := model.NewSystem(cfg.name) + + system.ApplyMeta(cfg.meta) + err = parseInputs(system, expanded) if err != nil { return err } - err = helper.MakeDir(outputDir) + err = helper.MakeDir(cfg.outputDir) if err != nil { return fmt.Errorf("error creating output directory: %w", err) } opts := gen.GeneratorOptions{ - OutputDir: outputDir, - TemplatesDir: templatesDir, + OutputDir: cfg.outputDir, + TemplatesDir: cfg.templatesDir, System: system, - UserFeatures: features, - UserForce: force, + UserFeatures: cfg.features, + UserForce: cfg.force, } generator, err := gen.New(opts) if err != nil { return err } - doc, err := gen.ReadRulesDoc(rulesFile) + doc, err := gen.ReadRulesDoc(cfg.rulesFile) if err != nil { return err } diff --git a/pkg/spec/schema/apigear.solution.schema.yaml b/pkg/spec/schema/apigear.solution.schema.yaml index ab6779c9..f701ee8b 100644 --- a/pkg/spec/schema/apigear.solution.schema.yaml +++ b/pkg/spec/schema/apigear.solution.schema.yaml @@ -24,7 +24,7 @@ properties: description: "The root directory of the solution to map all other paths to." meta: type: object - description: "The meta section contains meta data about the solution." + description: "The meta added to the system in the template." layers: type: array items: @@ -57,7 +57,7 @@ definitions: description: "the output directory of the layer." meta: type: object - description: "meta data about the layer which will be passed on to the template." + description: "meta data about the layer which will be passed on to the system in the template" template: type: string description: "path to the template which can be either template package name or a template folder with a rules document." diff --git a/pkg/spec/sol.go b/pkg/spec/sol.go index 7a10e89b..8b3974d8 100644 --- a/pkg/spec/sol.go +++ b/pkg/spec/sol.go @@ -5,13 +5,14 @@ import ( ) type SolutionLayer struct { - Name string `json:"name" yaml:"name"` - Description string `json:"description" yaml:"description"` - Inputs []string `json:"inputs" yaml:"inputs"` - Output string `json:"output" yaml:"output"` - Template string `json:"template" yaml:"template"` - Features []string `json:"features" yaml:"features"` - Force bool `json:"force" yaml:"force"` + Name string `json:"name" yaml:"name"` + Description string `json:"description" yaml:"description"` + Inputs []string `json:"inputs" yaml:"inputs"` + Output string `json:"output" yaml:"output"` + Template string `json:"template" yaml:"template"` + Features []string `json:"features" yaml:"features"` + Force bool `json:"force" yaml:"force"` + Meta map[string]interface{} `json:"meta" yaml:"meta"` } func (l *SolutionLayer) Validate() error { @@ -32,12 +33,14 @@ func (l *SolutionLayer) Validate() error { } type SolutionDoc struct { - Schema string `json:"schema" yaml:"schema"` - Version string `json:"version" yaml:"version"` - Name string `json:"name" yaml:"name"` - Description string `json:"description" yaml:"description"` - RootDir string `json:"rootDir" yaml:"rootDir"` - Layers []*SolutionLayer `json:"layers" yaml:"layers"` + Schema string `json:"schema" yaml:"schema"` + Version string `json:"version" yaml:"version"` + Name string `json:"name" yaml:"name"` + Description string `json:"description" yaml:"description"` + RootDir string `json:"rootDir" yaml:"rootDir"` + Layers []*SolutionLayer `json:"layers" yaml:"layers"` + Meta map[string]interface{} `json:"meta" yaml:"meta"` + MetaFile string `json:"metaFile" yaml:"metaFile"` } func (s *SolutionDoc) Validate() error {