1// SPDX-License-Identifier: MIT
2
3package workflow
4
5import (
6 "bnema.dev/ermite-actions/template"
7)
8
9// converter turns the token tree of a workflow into the model. It is a port
10// of the reference's converter, which reports problems by throwing an error
11// that the caller of each conversion step catches and records; here panics of
12// type *template.TokenError play the part of those exceptions.
13type converter struct {
14 ctx *template.Context
15 schema *template.Schema
16 w *Workflow
17 jobIDs *idBuilder
18}
19
20// handle runs fn and records a *template.TokenError panic as an error
21// located at the token it names, like handleTemplateTokenErrors.
22func (c *converter) handle(root template.Token, fn func()) {
23 defer func() {
24 if r := recover(); r != nil {
25 te, ok := r.(*template.TokenError)
26 if !ok {
27 panic(r)
28 }
29 if te.Token != nil {
30 c.ctx.ErrorAt(te.Token, te.Message)
31 } else {
32 c.ctx.ErrorAt(root, te.Message)
33 }
34 }
35 }()
36 fn()
37}
38
39func ck[T any](v T, err error) T {
40 if err != nil {
41 panic(err)
42 }
43 return v
44}
45
46func asString(t template.Token, desc string) *template.String {
47 return ck(template.AssertString(t, desc))
48}
49
50func asBool(t template.Token, desc string) *template.Boolean {
51 return ck(template.AssertBoolean(t, desc))
52}
53
54func asSeq(t template.Token, desc string) *template.Sequence {
55 return ck(template.AssertSequence(t, desc))
56}
57
58func asMap(t template.Token, desc string) *template.Mapping {
59 return ck(template.AssertMapping(t, desc))
60}
61
62func asScalar(t template.Token, desc string) template.Token {
63 return ck(template.AssertScalar(t, desc))
64}
65
66func origin(t template.Token) template.Origin {
67 return template.Origin{File: t.File(), Pos: t.Pos()}
68}
69
70// convertRoot converts the root mapping.
71func (c *converter) convertRoot(root template.Token) {
72 defer func() {
73 if r := recover(); r != nil {
74 te, ok := r.(*template.TokenError)
75 if !ok {
76 panic(r)
77 }
78 if te.Token != nil {
79 c.ctx.ErrorAt(te.Token, te.Message)
80 } else {
81 c.ctx.ErrorAt(root, te.Message)
82 }
83 }
84 }()
85 m := asMap(root, "root")
86 for _, p := range m.Pairs {
87 key := asString(p.Key, "root key")
88 switch key.Value {
89 case "on":
90 c.handle(root, func() { c.w.On = c.convertOn(p.Value) })
91 case "jobs":
92 c.w.Jobs = nil
93 c.handle(root, func() { c.w.Jobs = c.convertJobs(p.Value) })
94 case "concurrency":
95 c.handle(root, func() { c.convertConcurrency(p.Value) })
96 c.w.Concurrency = p.Value
97 case "env":
98 if em, ok := p.Value.(*template.Mapping); ok {
99 c.w.Env = em
100 }
101 case "name":
102 if s, ok := p.Value.(*template.String); ok {
103 c.w.Name = s.Value
104 }
105 case "run-name":
106 c.w.RunName = p.Value
107 case "defaults":
108 c.w.Defaults = convertDefaults(p.Value)
109 case "permissions":
110 c.w.Permissions = convertPermissions(p.Value)
111 }
112 }
113}
114
115func convertDefaults(t template.Token) *Defaults {
116 m, ok := t.(*template.Mapping)
117 if !ok {
118 return nil
119 }
120 d := &Defaults{}
121 if run, ok := m.Find("run").(*template.Mapping); ok {
122 d.Run = &RunDefaults{Shell: run.Find("shell"), WorkingDirectory: run.Find("working-directory")}
123 }
124 return d
125}
126
127func convertPermissions(t template.Token) *Permissions {
128 switch t := t.(type) {
129 case *template.String:
130 return &Permissions{All: t.Value}
131 case *template.Mapping:
132 p := &Permissions{}
133 for _, kv := range t.Pairs {
134 p.Scopes = append(p.Scopes, PermissionScope{Scope: kv.Key.String(), Level: kv.Value.String()})
135 }
136 return p
137 }
138 return nil
139}