diff --git a/parser/caller.go b/parser/caller.go index cdc8f300..859a3230 100644 --- a/parser/caller.go +++ b/parser/caller.go @@ -11,6 +11,7 @@ import ( "github.com/blackstork-io/fabric/parser/definitions" "github.com/blackstork-io/fabric/parser/evaluation" + "github.com/blackstork-io/fabric/parser/plugincaller" "github.com/blackstork-io/fabric/pkg/diagnostics" "github.com/blackstork-io/fabric/pkg/utils" "github.com/blackstork-io/fabric/plugin" @@ -21,10 +22,6 @@ type pluginData struct { ConfigSpec hcldec.Spec InvocationSpec hcldec.Spec } -type PluginCaller interface { - CallContent(name string, config evaluation.Configuration, invocation evaluation.Invocation, context map[string]any) (result string, diag diagnostics.Diag) - CallData(name string, config evaluation.Configuration, invocation evaluation.Invocation) (result map[string]any, diag diagnostics.Diag) -} type Caller struct { plugins *runner.Runner @@ -36,7 +33,7 @@ func NewPluginCaller(r *runner.Runner) *Caller { } } -var _ PluginCaller = (*Caller)(nil) +var _ plugincaller.PluginCaller = (*Caller)(nil) func (c *Caller) pluginData(kind, name string) (pluginData, diagnostics.Diag) { switch kind { @@ -69,35 +66,27 @@ func (c *Caller) pluginData(kind, name string) (pluginData, diagnostics.Diag) { } } -func (c *Caller) callPlugin(kind, name string, config evaluation.Configuration, invocation evaluation.Invocation, dataCtx map[string]any) (res any, diags diagnostics.Diag) { +func (c *Caller) callPlugin(ctx context.Context, kind, name string, config evaluation.Configuration, invocation evaluation.Invocation, dataCtx plugin.MapData) (res any, diags diagnostics.Diag) { data, diags := c.pluginData(kind, name) if diags.HasErrors() { return } - dataCtxAny, err := plugin.ParseDataMapAny(dataCtx) - if err != nil { - diags.Add("Error while parsing context", err.Error()) - return - } - acceptsConfig := !utils.IsNil(data.ConfigSpec) hasConfig := config.Exists() var configVal cty.Value if acceptsConfig { - var stdDiag diagnostics.Diag - configVal, stdDiag = config.ParseConfig(data.ConfigSpec) - if !diags.Extend(stdDiag) { + var diag diagnostics.Diag + configVal, diag = config.ParseConfig(data.ConfigSpec) + if !diags.Extend(diag) { typ := hcldec.ImpliedType(data.ConfigSpec) errs := configVal.Type().TestConformance(typ) if errs != nil { // Attempt a conversion var err error configVal, err = convert.Convert(configVal, typ) - if err != nil { - diags.AppendErr(err, "Error while serializing config") - } + diags.AppendErr(err, "Error while serializing config") } } } else if hasConfig { @@ -114,9 +103,6 @@ func (c *Caller) callPlugin(kind, name string, config evaluation.Configuration, pluginArgs, diag := invocation.ParseInvocation(data.InvocationSpec) diags.Extend(diag) - if diag.HasErrors() { - return - } if data.InvocationSpec != nil { typ := hcldec.ImpliedType(data.InvocationSpec) errs := pluginArgs.Type().TestConformance(typ) @@ -124,84 +110,68 @@ func (c *Caller) callPlugin(kind, name string, config evaluation.Configuration, // Attempt a conversion var err error pluginArgs, err = convert.Convert(pluginArgs, typ) - if err != nil { - diag.AppendErr(err, "Error while serializing args") - - return nil, diag - } + diags.AppendErr(err, "Error while serializing args") } } - - var result struct { - Result any - Diags hcl.Diagnostics + if diags.HasErrors() { + return } + switch kind { case "data": - source, diags := c.plugins.DataSource(name) - if diags.HasErrors() { - return nil, diagnostics.Diag(diags) + source, diag := c.plugins.DataSource(name) + if diags.ExtendHcl(diag) { + return } - data, diags := source.Execute(context.Background(), &plugin.RetrieveDataParams{ + data, diag := source.Execute(ctx, &plugin.RetrieveDataParams{ Config: configVal, Args: pluginArgs, }) - if data != nil { - result.Result = data.Any() - } - result.Diags = diags + res = data + diags.ExtendHcl(diag) case "content": - provider, diags := c.plugins.ContentProvider(name) - if diags.HasErrors() { - return nil, diagnostics.Diag(diags) + provider, diag := c.plugins.ContentProvider(name) + if diags.ExtendHcl(diag) { + return } - content, diags := provider.Execute(context.Background(), &plugin.ProvideContentParams{ + content, diag := provider.Execute(ctx, &plugin.ProvideContentParams{ Config: configVal, Args: pluginArgs, - DataContext: dataCtxAny, + DataContext: dataCtx, }) - result.Result = "" + res = "" if content != nil { - result.Result = content.Markdown + res = content.Markdown } - result.Diags = diags - } - - for _, d := range result.Diags { - diags = append(diags, d) + diags.ExtendHcl(diag) } - res = result.Result return } -func (c *Caller) CallContent(name string, config evaluation.Configuration, invocation evaluation.Invocation, context map[string]any) (result string, diag diagnostics.Diag) { +func (c *Caller) CallContent(ctx context.Context, name string, config evaluation.Configuration, invocation evaluation.Invocation, dataCtx plugin.MapData) (result string, diag diagnostics.Diag) { var ok bool var res any - res, diag = c.callPlugin(definitions.BlockKindContent, name, config, invocation, context) + res, diag = c.callPlugin(ctx, definitions.BlockKindContent, name, config, invocation, dataCtx) + if diag.HasErrors() { + return + } result, ok = res.(string) - if !diag.HasErrors() && !ok { - diag.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Incorrect result type", - Detail: "Plugin returned incorrect data type. Please contact fabric developers about this issue", - Subject: invocation.DefRange().Ptr(), - }) + if !ok { + panic("Incorrect plugin result type") } return } -func (c *Caller) CallData(name string, config evaluation.Configuration, invocation evaluation.Invocation) (result map[string]any, diag diagnostics.Diag) { +func (c *Caller) CallData(ctx context.Context, name string, config evaluation.Configuration, invocation evaluation.Invocation) (result plugin.MapData, diag diagnostics.Diag) { var ok bool var res any - res, diag = c.callPlugin(definitions.BlockKindData, name, config, invocation, nil) - result, ok = res.(map[string]any) - if !diag.HasErrors() && !ok { - diag.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Incorrect result type", - Detail: "Plugin returned incorrect data type. Please contact fabric developers about this issue", - Subject: invocation.DefRange().Ptr(), - }) + res, diag = c.callPlugin(ctx, definitions.BlockKindData, name, config, invocation, nil) + if diag.HasErrors() { + return + } + result, ok = res.(plugin.MapData) + if !ok { + panic("Incorrect plugin result type") } return } diff --git a/parser/definedBlocks.go b/parser/definedBlocks.go index 9d9e3688..4ddf8dc8 100644 --- a/parser/definedBlocks.go +++ b/parser/definedBlocks.go @@ -98,9 +98,13 @@ func (db *DefinedBlocks) AsValueMap() map[string]cty.Value { } func (db *DefinedBlocks) DefaultConfigFor(plugin *definitions.Plugin) (config *definitions.Config) { + return db.DefaultConfig(plugin.Kind(), plugin.Name()) +} + +func (db *DefinedBlocks) DefaultConfig(pluginKind, pluginName string) (config *definitions.Config) { return db.Config[definitions.Key{ - PluginKind: plugin.Kind(), - PluginName: plugin.Name(), + PluginKind: pluginKind, + PluginName: pluginName, BlockName: "", }] } diff --git a/parser/definitions/meta.go b/parser/definitions/meta.go index 02666db3..b5a17e1a 100644 --- a/parser/definitions/meta.go +++ b/parser/definitions/meta.go @@ -1,5 +1,7 @@ package definitions +import "github.com/blackstork-io/fabric/plugin" + type MetaBlock struct { // XXX: is empty sting enougth or use a proper ptr-nil-if-missing? Author string `hcl:"author,optional"` @@ -7,3 +9,14 @@ type MetaBlock struct { // TODO: ?store def range defRange hcl.Range } + +func (m *MetaBlock) AsJQ() plugin.Data { + tags := make(plugin.ListData, len(m.Tags)) + for i, tag := range m.Tags { + tags[i] = plugin.StringData(tag) + } + return plugin.ConvMapData{ + "author": plugin.StringData(m.Author), + "tags": tags, + } +} diff --git a/parser/definitions/section.go b/parser/definitions/section.go index d90113fa..cbd2fabd 100644 --- a/parser/definitions/section.go +++ b/parser/definitions/section.go @@ -29,7 +29,7 @@ var _ ContentPluginOrSection = (*ParsedPlugin)(nil) type ParsedSection struct { Meta *MetaBlock - Title *hclsyntax.Attribute + Title *ParsedPlugin Content []ContentPluginOrSection } diff --git a/parser/evaluator.go b/parser/evaluator.go index 6f061240..acd4d9aa 100644 --- a/parser/evaluator.go +++ b/parser/evaluator.go @@ -1,137 +1,52 @@ package parser import ( + "context" "fmt" - "maps" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/gohcl" - "github.com/hashicorp/hcl/v2/hcldec" - "github.com/itchyny/gojq" - "github.com/zclconf/go-cty/cty" "github.com/blackstork-io/fabric/parser/definitions" + evaltree "github.com/blackstork-io/fabric/parser/parsetree" + "github.com/blackstork-io/fabric/parser/plugincaller" "github.com/blackstork-io/fabric/pkg/diagnostics" - "github.com/blackstork-io/fabric/pkg/jsontools" - "github.com/blackstork-io/fabric/pkg/utils" + "github.com/blackstork-io/fabric/plugin" ) // Evaluates a chosen document type Evaluator struct { - caller PluginCaller - contentCalls []*definitions.ParsedPlugin + caller plugincaller.PluginCaller topLevelBlocks *DefinedBlocks - context map[string]any + context plugin.MapData } -func NewEvaluator(caller PluginCaller, blocks *DefinedBlocks) *Evaluator { +func NewEvaluator(caller plugincaller.PluginCaller, blocks *DefinedBlocks) *Evaluator { return &Evaluator{ caller: caller, topLevelBlocks: blocks, - context: map[string]any{}, + context: plugin.MapData{}, } } -func (e *Evaluator) evaluateQuery(call *definitions.ParsedPlugin) (context map[string]any, diags diagnostics.Diag) { - context = e.context - body := call.Invocation.GetBody() - queryAttr, found := body.Attributes["query"] - if !found { - return - } - val, newBody, dgs := hcldec.PartialDecode(body, &hcldec.ObjectSpec{ - "query": &hcldec.AttrSpec{ - Name: "query", - Type: cty.String, - Required: true, - }, - }, nil) - call.Invocation.SetBody(utils.ToHclsyntaxBody(newBody)) - if diags.ExtendHcl(dgs) { - return - } - query := val.GetAttr("query").AsString() - q, err := gojq.Parse(query) - if err != nil { - diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Failed to parse the query", - Detail: err.Error(), - Subject: &queryAttr.SrcRange, - }) - return - } - - code, err := gojq.Compile(q) - if err != nil { - diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Failed to compile the query", - Detail: err.Error(), - Subject: &queryAttr.SrcRange, - }) - return - } - queryResultIter := code.Run(context) - queryResult, ok := queryResultIter.Next() - if ok { - context = maps.Clone(context) - context["query_result"] = queryResult - } - return -} - func (e *Evaluator) EvaluateDocument(d *definitions.Document) (results []string, diags diagnostics.Diag) { - // TODO: Combining parsing and evaluation of the document is flawed - // perhaps a simpler approach is using more steps? - // Currently: - // 1) Define - // 2) ParseAndEvaluate (data) - // 3) Evaluate (content) - // This introduces compelexity with local content context (such as meta blocks) - // Switch to: - // 1) Define - // 2) ParseDocument - // 3) Evaluate (data) - // 4) Evaluate (content) - // May be a bit slower, but allows us to have access to full evaluation context at each step - - diags = e.parseAndEvaluateDocument(d) + node, diags := e.docToEvalTree(d) if diags.HasErrors() { return } - - results = make([]string, 0, len(e.contentCalls)) - for _, call := range e.contentCalls { - context, _ := e.evaluateQuery(call) - // if diags.Extend(diag) { - // query failed, but context is always valid - // TODO: #28 #29 - // } - result, diag := e.caller.CallContent(call.PluginName, call.Config, call.Invocation, context) - if diags.Extend(diag) { - // XXX: What to do if we have errors while executing content blocks? - // just skipping the value for now... - continue - } - results = append(results, result) - // TODO: Here's the place to implement local context #17 - // However I think we need to rework it a bit before done - } + results, diag := node.EvalContent(context.TODO(), e.caller) + diags.Extend(diag) return } -func (e *Evaluator) parseAndEvaluateDocument(d *definitions.Document) (diags diagnostics.Diag) { +func (e *Evaluator) docToEvalTree(d *definitions.Document) (node *evaltree.DocumentNode, diags diagnostics.Diag) { + node = new(evaltree.DocumentNode) if title := d.Block.Body.Attributes["title"]; title != nil { pluginName := "text" - e.contentCalls = append(e.contentCalls, &definitions.ParsedPlugin{ + node.AddContent(&definitions.ParsedPlugin{ PluginName: pluginName, - Config: e.topLevelBlocks.Config[definitions.Key{ - PluginKind: definitions.BlockKindContent, - PluginName: pluginName, - BlockName: "", - }], // use default config + Config: e.topLevelBlocks.DefaultConfig(definitions.BlockKindContent, pluginName), Invocation: definitions.NewTitle(title.Expr), }) } @@ -151,26 +66,9 @@ func (e *Evaluator) parseAndEvaluateDocument(d *definitions.Document) (diags dia } switch block.Type { case definitions.BlockKindContent: - // delaying content calls until all data calls are completed - // TODO: contentCalls must also store a ref to context (here - to the meta of the document) - // also requires to parse meta first, not in declaration order - e.contentCalls = append(e.contentCalls, call) + node.AddContent(call) case definitions.BlockKindData: - res, diag := e.caller.CallData( - call.PluginName, - call.Config, - call.Invocation, - ) - if diags.Extend(diag) { - continue - } - var err error - e.context, err = jsontools.MapSet(e.context, []string{ - definitions.BlockKindData, - call.PluginName, - call.BlockName, - }, res) - diags.AppendErr(err, "Failed to save data plugin result") + node.AddData(call) default: panic("must be exhaustive") } @@ -193,7 +91,7 @@ func (e *Evaluator) parseAndEvaluateDocument(d *definitions.Document) (diags dia if diags.ExtendHcl(gohcl.DecodeBody(block.Body, nil, &meta)) { continue } - d.Meta = &meta + node.AddMeta(&meta) origMeta = block.DefRange().Ptr() case definitions.BlockKindSection: section, diag := definitions.DefineSection(block, false) @@ -204,7 +102,7 @@ func (e *Evaluator) parseAndEvaluateDocument(d *definitions.Document) (diags dia if diags.Extend(diag) { continue } - e.evaluateSection(parsedSection) + node.AddSection(parsedSection) default: diags.Append(definitions.NewNestingDiag( d.Block.Type, @@ -223,30 +121,3 @@ func (e *Evaluator) parseAndEvaluateDocument(d *definitions.Document) (diags dia return } - -func (e *Evaluator) evaluateSection(s *definitions.ParsedSection) { - if title := s.Title; title != nil { - pluginName := "text" - e.contentCalls = append(e.contentCalls, &definitions.ParsedPlugin{ - PluginName: pluginName, - Config: e.topLevelBlocks.Config[definitions.Key{ - PluginKind: definitions.BlockKindContent, - PluginName: pluginName, - BlockName: "", - }], // use default config - Invocation: definitions.NewTitle(title.Expr), - }) - } - - for _, content := range s.Content { - switch contentT := content.(type) { - case *definitions.ParsedPlugin: - // TODO: contentCalls must also store a ref to context (here - to the meta of the section) - e.contentCalls = append(e.contentCalls, contentT) - case *definitions.ParsedSection: - e.evaluateSection(contentT) - default: - panic("must be exhaustive") - } - } -} diff --git a/parser/mockPluginCaller.go b/parser/mockPluginCaller.go deleted file mode 100644 index c3dc6a28..00000000 --- a/parser/mockPluginCaller.go +++ /dev/null @@ -1,80 +0,0 @@ -package parser - -import ( - "strings" - - "github.com/sanity-io/litter" - "golang.org/x/exp/maps" - - "github.com/blackstork-io/fabric/parser/definitions" - "github.com/blackstork-io/fabric/parser/evaluation" - "github.com/blackstork-io/fabric/pkg/diagnostics" - "github.com/blackstork-io/fabric/pkg/utils" -) - -type MockCaller struct{} - -func (c *MockCaller) dumpContext(context map[string]any) string { - return litter.Sdump("Context:", context) -} - -func (c *MockCaller) dumpConfig(config evaluation.Configuration) string { - if utils.IsNil(config) { - return "NoConfig" - } - switch c := config.(type) { - case *definitions.ConfigPtr: - attrs, _ := c.Cfg.Body.JustAttributes() - return litter.Sdump("ConfigPtr", maps.Keys(attrs)) - case *definitions.Config: - attrs, _ := c.Block.Body.JustAttributes() - return litter.Sdump("Config", maps.Keys(attrs)) - default: - return "UnknownConfig " + litter.Sdump(c) - } -} - -func (c *MockCaller) dumpInvocation(invoke evaluation.Invocation) string { - if utils.IsNil(invoke) { - return "NoConfig" - } - switch inv := invoke.(type) { - case *evaluation.BlockInvocation: - attrStringed := map[string]string{} - attrs, _ := inv.Body.JustAttributes() - for k, v := range attrs { - val, _ := v.Expr.Value(nil) - attrStringed[k] = val.GoString() - } - - return litter.Sdump("BlockInvocation", attrStringed) - case *definitions.TitleInvocation: - val, _ := inv.Expression.Value(nil) - return litter.Sdump("TitleInvocation", val.GoString()) - default: - return "UnknownInvocation " + litter.Sdump(inv) - } -} - -// CallContent implements PluginCaller. -func (c *MockCaller) CallContent(name string, config evaluation.Configuration, invocation evaluation.Invocation, context map[string]any) (result string, diag diagnostics.Diag) { - dump := []string{ - "Call to content:", - } - dump = append(dump, c.dumpConfig(config)) - dump = append(dump, c.dumpInvocation(invocation)) - dump = append(dump, c.dumpContext(context)) - return strings.Join(dump, "\n") + "\n\n", nil -} - -// CallData implements PluginCaller. -func (c *MockCaller) CallData(name string, config evaluation.Configuration, invocation evaluation.Invocation) (result map[string]any, diag diagnostics.Diag) { - dump := []string{ - "Call to data:", - } - dump = append(dump, c.dumpConfig(config)) - dump = append(dump, c.dumpInvocation(invocation)) - return map[string]any{"dumpResult": strings.Join(dump, "\n")}, nil -} - -var _ PluginCaller = (*MockCaller)(nil) diff --git a/parser/parseSectionBlock.go b/parser/parseSectionBlock.go index d6bdbc71..d65b7801 100644 --- a/parser/parseSectionBlock.go +++ b/parser/parseSectionBlock.go @@ -47,8 +47,14 @@ func (db *DefinedBlocks) ParseSection(section *definitions.Section) (res *defini } func (db *DefinedBlocks) parseSection(section *definitions.Section) (parsed *definitions.ParsedSection, diags diagnostics.Diag) { - res := definitions.ParsedSection{ - Title: section.Block.Body.Attributes["title"], + res := definitions.ParsedSection{} + if title := section.Block.Body.Attributes["title"]; title != nil { + pluginName := "text" + res.Title = &definitions.ParsedPlugin{ + PluginName: pluginName, + Config: db.DefaultConfig(definitions.BlockKindContent, pluginName), + Invocation: definitions.NewTitle(title.Expr), + } } var origMeta *hcl.Range diff --git a/parser/parsetree/parsetree.go b/parser/parsetree/parsetree.go new file mode 100644 index 00000000..23a017f7 --- /dev/null +++ b/parser/parsetree/parsetree.go @@ -0,0 +1,244 @@ +package parsetree + +import ( + "context" + "fmt" + "maps" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hcldec" + "github.com/itchyny/gojq" + "github.com/zclconf/go-cty/cty" + + "github.com/blackstork-io/fabric/parser/definitions" + "github.com/blackstork-io/fabric/parser/plugincaller" + "github.com/blackstork-io/fabric/pkg/diagnostics" + "github.com/blackstork-io/fabric/pkg/utils" + "github.com/blackstork-io/fabric/plugin" +) + +type DocumentNode struct { + meta *definitions.MetaBlock + contentNodes []Renderable + dataNodes []*definitions.ParsedPlugin +} + +func (dn *DocumentNode) AddContent(content *definitions.ParsedPlugin) { + dn.contentNodes = append(dn.contentNodes, ContentNode{plugin: content}) +} + +func (dn *DocumentNode) AddSection(section *definitions.ParsedSection) { + dn.contentNodes = append(dn.contentNodes, SectionNode{section: section}) +} + +func (dn *DocumentNode) AddData(data *definitions.ParsedPlugin) { + dn.dataNodes = append(dn.dataNodes, data) +} + +func (dn *DocumentNode) AddMeta(meta *definitions.MetaBlock) { + dn.meta = meta +} + +// result has a shape map[plugin_name]map[block_name]plugin_result. +func (dn *DocumentNode) EvalData(ctx context.Context, caller plugincaller.DataCaller) (result plugin.MapData, diags diagnostics.Diag) { + // TODO: can be parallel: + // TODO: once again: meta blocks are not used + + result = plugin.MapData{} + for _, node := range dn.dataNodes { + res, diag := caller.CallData( + ctx, + node.PluginName, + node.Config, + node.Invocation, + ) + if diags.Extend(diag) { + continue + } + + var pluginNameRes plugin.MapData + if m, found := result[node.PluginName]; found { + pluginNameRes = m.(plugin.MapData) + } else { + pluginNameRes = plugin.MapData{} + result[node.PluginName] = pluginNameRes + } + pluginNameRes[node.BlockName] = res + } + return +} + +type resultsList struct { + ptr *[]string +} + +func (d resultsList) AsJQ() plugin.Data { + lst := *d.ptr + dst := make([]plugin.Data, len(lst)) + for i, v := range lst { + dst[i] = plugin.StringData(v) + } + return plugin.ListData(dst) +} + +func (dn *DocumentNode) EvalContent(ctx context.Context, caller plugincaller.PluginCaller) (result []string, diags diagnostics.Diag) { + dataResult, diags := dn.EvalData(ctx, caller) + if diags.HasErrors() { + return + } + + document := plugin.ConvMapData{ + definitions.BlockKindContent: resultsList{ptr: &result}, + } + if dn.meta != nil { + document[definitions.BlockKindMeta] = dn.meta.AsJQ() + } + + globalCtx := plugin.ConvMapData{ + definitions.BlockKindData: dataResult, + definitions.BlockKindDocument: document, + } + + for _, content := range dn.contentNodes { + localCtx := maps.Clone(globalCtx) + diags.Extend( + content.Render(ctx, caller, localCtx, &result), + ) + } + return +} + +type ContentNode struct { + plugin *definitions.ParsedPlugin +} + +// Render implements Renderable. +func (c ContentNode) Render(ctx context.Context, caller plugincaller.ContentCaller, localCtx plugin.ConvMapData, result *[]string) (diags diagnostics.Diag) { + if c.plugin.Meta != nil { + localCtx[definitions.BlockKindContent] = plugin.ConvMapData{ + definitions.BlockKindMeta: c.plugin.Meta.AsJQ(), + } + } + + query, found, rng, diag := c.GetQuery() + if !diags.Extend(diag) && found { + diags.Extend(ExecuteQuery(query, rng, localCtx)) + } + // TODO: #28 #29 + if diags.HasErrors() { + return + } + + resultStr, diag := caller.CallContent(ctx, c.plugin.PluginName, c.plugin.Config, c.plugin.Invocation, localCtx.AsJQ().(plugin.MapData)) + if diags.Extend(diag) { + // XXX: What to do if we have errors while executing content blocks? + // just skipping the value for now... + return + } + *result = append(*result, resultStr) + return +} + +// GetQuery implements Queriable. +func (c ContentNode) GetQuery() (query string, found bool, rng *hcl.Range, diags diagnostics.Diag) { + body := c.plugin.Invocation.GetBody() + attr, found := body.Attributes["query"] + if !found { + return + } + rng = &attr.SrcRange + val, newBody, dgs := hcldec.PartialDecode(body, &hcldec.ObjectSpec{ + "query": &hcldec.AttrSpec{ + Name: "query", + Type: cty.String, + Required: true, + }, + }, nil) + c.plugin.Invocation.SetBody(utils.ToHclsyntaxBody(newBody)) + if diags.ExtendHcl(dgs) { + return + } + query = val.GetAttr("query").AsString() + return +} + +var _ Queriable = ContentNode{} + +type SectionNode struct { + section *definitions.ParsedSection +} + +// Render implements Renderable. +func (s SectionNode) Render(ctx context.Context, caller plugincaller.ContentCaller, globalCtx plugin.ConvMapData, result *[]string) (diags diagnostics.Diag) { + localCtx := maps.Clone(globalCtx) + if s.section.Meta != nil { + localCtx[definitions.BlockKindSection] = plugin.ConvMapData{ + definitions.BlockKindMeta: s.section.Meta.AsJQ(), + } + } + if title := s.section.Title; title != nil { + diags.Extend(ContentNode{plugin: title}.Render(ctx, caller, localCtx, result)) + } + + for _, content := range s.section.Content { + switch contentT := content.(type) { + case *definitions.ParsedPlugin: + localLocalCtx := maps.Clone(localCtx) + content := ContentNode{plugin: contentT} + diags.Extend( + content.Render(ctx, caller, localLocalCtx, result), + ) + case *definitions.ParsedSection: + diags.Extend(SectionNode{section: contentT}.Render(ctx, caller, localCtx, result)) + default: + panic("must be exhaustive") + } + } + return +} + +func ExecuteQuery(query string, rng *hcl.Range, localCtx plugin.ConvMapData) (diags diagnostics.Diag) { + localCtx["query"] = plugin.StringData(query) + queryResult, err := runQuery(query, localCtx) + if err != nil { + diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Failed to execute the query", + Detail: err.Error(), + Subject: rng, + }) + return + } + localCtx["query_result"] = queryResult + return +} + +func runQuery(query string, dataCtx plugin.ConvMapData) (queryResult plugin.Data, err error) { + q, err := gojq.Parse(query) + if err != nil { + err = fmt.Errorf("failed to parse the query: %w", err) + return + } + + code, err := gojq.Compile(q) + if err != nil { + err = fmt.Errorf("failed to compile the query: %w", err) + return + } + res, hasResult := code.Run(dataCtx.Any()).Next() + if hasResult { + queryResult, err = plugin.ParseDataAny(res) + if err != nil { + err = fmt.Errorf("incorrect query result type: %w", err) + } + } + return +} + +type Renderable interface { + Render(ctx context.Context, caller plugincaller.ContentCaller, localCtx plugin.ConvMapData, result *[]string) diagnostics.Diag +} + +type Queriable interface { + GetQuery() (query string, found bool, rng *hcl.Range, diags diagnostics.Diag) +} diff --git a/parser/plugincaller/plugincaller.go b/parser/plugincaller/plugincaller.go new file mode 100644 index 00000000..2c7e9014 --- /dev/null +++ b/parser/plugincaller/plugincaller.go @@ -0,0 +1,22 @@ +package plugincaller + +import ( + "context" + + "github.com/blackstork-io/fabric/parser/evaluation" + "github.com/blackstork-io/fabric/pkg/diagnostics" + "github.com/blackstork-io/fabric/plugin" +) + +type DataCaller interface { + CallData(ctx context.Context, name string, config evaluation.Configuration, invocation evaluation.Invocation) (result plugin.MapData, diag diagnostics.Diag) +} + +type ContentCaller interface { + CallContent(ctx context.Context, name string, config evaluation.Configuration, invocation evaluation.Invocation, context plugin.MapData) (result string, diag diagnostics.Diag) +} + +type PluginCaller interface { + DataCaller + ContentCaller +} diff --git a/plugin/data.go b/plugin/data.go index 1166cbd2..8919ffda 100644 --- a/plugin/data.go +++ b/plugin/data.go @@ -8,6 +8,7 @@ import ( type Data interface { Any() any data() + ConvertableData } func (NumberData) data() {} @@ -16,6 +17,12 @@ func (BoolData) data() {} func (MapData) data() {} func (ListData) data() {} +func (d NumberData) AsJQ() Data { return d } +func (d StringData) AsJQ() Data { return d } +func (d BoolData) AsJQ() Data { return d } +func (d MapData) AsJQ() Data { return d } +func (d ListData) AsJQ() Data { return d } + type NumberData float64 func (d NumberData) Any() any { @@ -105,6 +112,9 @@ func ParseDataAny(v any) (Data, error) { case string: return StringData(v), nil case []any: + // TODO: potential bug: + // this case would trigger only for []any, not, for example, []string + // this can be worked around using reflection dst := make(ListData, len(v)) for i, e := range v { d, err := ParseDataAny(e) @@ -135,3 +145,49 @@ func ParseDataMapAny(v map[string]any) (MapData, error) { } return dst, nil } + +type ConvertableData interface { + AsJQ() Data +} + +type ConvMapData map[string]ConvertableData + +func (d ConvMapData) AsJQ() Data { + dst := make(MapData, len(d)) + for k, v := range d { + if v == nil { + dst[k] = nil + } else { + dst[k] = v.AsJQ() + } + } + return dst +} + +func (d ConvMapData) Any() any { + dst := make(map[string]any, len(d)) + for k, v := range d { + dst[k] = v.AsJQ().Any() + } + return dst +} +func (d ConvMapData) data() {} + +type ConvListData []ConvertableData + +func (d ConvListData) AsJQ() Data { + dst := make(ListData, len(d)) + for k, v := range d { + dst[k] = v.AsJQ() + } + return dst +} + +func (d ConvListData) Any() any { + dst := make([]any, len(d)) + for k, v := range d { + dst[k] = v.AsJQ().Any() + } + return dst +} +func (d ConvListData) data() {} diff --git a/test/e2e/render_test.go b/test/e2e/render_test.go index 4c2a171e..2d4a42a1 100644 --- a/test/e2e/render_test.go +++ b/test/e2e/render_test.go @@ -397,4 +397,77 @@ func TestE2ERender(t *testing.T) { []string{"There are 3 items"}, [][]diag_test.Assert{}, ) + renderTest( + t, "Document meta", + []string{ + ` + document "test" { + meta { + author = "foo" + } + content text { + query = ".document.meta.author" + text = "author = {{ .query_result }}" + } + } + `, + }, + "test", + []string{"author = foo"}, + [][]diag_test.Assert{}, + ) + renderTest( + t, "Document and content meta", + []string{ + ` + document "test" { + meta { + author = "foo" + } + section { + meta { + author = "bar" + } + content text { + meta { + author = "baz" + } + query = "(.document.meta.author + .section.meta.author + .content.meta.author)" // + text = "author = {{ .query_result }}" + } + } + } + `, + }, + "test", + []string{"author = foobarbaz"}, + [][]diag_test.Assert{}, + ) + renderTest( + t, "Reference rendered blocks", + []string{ + ` + document "test" { + content text { + text = "first result" + } + content text { + query = ".document.content[0]" + text = "content[0] = {{ .query_result }}" + } + content text { + query = ".document.content[1]" + text = "content[1] = {{ .query_result }}" + } + } + `, + }, + "test", + []string{ + "first result", + "content[0] = first result", + "content[1] = content[0] = first result", + }, + [][]diag_test.Assert{}, + ) }