1package expr
2
3import (
4	"context"
5	"fmt"
6	"strings"
7	"time"
8
9	"github.com/grafana/grafana-plugin-sdk-go/backend/gtime"
10	"github.com/grafana/grafana/pkg/expr/mathexp"
11)
12
13// Command is an interface for all expression commands.
14type Command interface {
15	NeedsVars() []string
16	Execute(c context.Context, vars mathexp.Vars) (mathexp.Results, error)
17}
18
19// MathCommand is a command for a math expression such as "1 + $GA / 2"
20type MathCommand struct {
21	RawExpression string
22	Expression    *mathexp.Expr
23	refID         string
24}
25
26// NewMathCommand creates a new MathCommand. It will return an error
27// if there is an error parsing expr.
28func NewMathCommand(refID, expr string) (*MathCommand, error) {
29	parsedExpr, err := mathexp.New(expr)
30	if err != nil {
31		return nil, err
32	}
33	return &MathCommand{
34		RawExpression: expr,
35		Expression:    parsedExpr,
36		refID:         refID,
37	}, nil
38}
39
40// UnmarshalMathCommand creates a MathCommand from Grafana's frontend query.
41func UnmarshalMathCommand(rn *rawNode) (*MathCommand, error) {
42	rawExpr, ok := rn.Query["expression"]
43	if !ok {
44		return nil, fmt.Errorf("math command for refId %v is missing an expression", rn.RefID)
45	}
46	exprString, ok := rawExpr.(string)
47	if !ok {
48		return nil, fmt.Errorf("expected math command for refId %v expression to be a string, got %T", rn.RefID, rawExpr)
49	}
50
51	gm, err := NewMathCommand(rn.RefID, exprString)
52	if err != nil {
53		return nil, fmt.Errorf("invalid math command type in '%v': %v", rn.RefID, err)
54	}
55	return gm, nil
56}
57
58// NeedsVars returns the variable names (refIds) that are dependencies
59// to execute the command and allows the command to fulfill the Command interface.
60func (gm *MathCommand) NeedsVars() []string {
61	return gm.Expression.VarNames
62}
63
64// Execute runs the command and returns the results or an error if the command
65// failed to execute.
66func (gm *MathCommand) Execute(ctx context.Context, vars mathexp.Vars) (mathexp.Results, error) {
67	return gm.Expression.Execute(gm.refID, vars)
68}
69
70// ReduceCommand is an expression command for reduction of a timeseries such as a min, mean, or max.
71type ReduceCommand struct {
72	Reducer     string
73	VarToReduce string
74	refID       string
75}
76
77// NewReduceCommand creates a new ReduceCMD.
78func NewReduceCommand(refID, reducer, varToReduce string) *ReduceCommand {
79	// TODO: validate reducer here, before execution
80	return &ReduceCommand{
81		Reducer:     reducer,
82		VarToReduce: varToReduce,
83		refID:       refID,
84	}
85}
86
87// UnmarshalReduceCommand creates a MathCMD from Grafana's frontend query.
88func UnmarshalReduceCommand(rn *rawNode) (*ReduceCommand, error) {
89	rawVar, ok := rn.Query["expression"]
90	if !ok {
91		return nil, fmt.Errorf("no variable specified to reduce for refId %v", rn.RefID)
92	}
93	varToReduce, ok := rawVar.(string)
94	if !ok {
95		return nil, fmt.Errorf("expected reduce variable to be a string, got %T for refId %v", rawVar, rn.RefID)
96	}
97	varToReduce = strings.TrimPrefix(varToReduce, "$")
98
99	rawReducer, ok := rn.Query["reducer"]
100	if !ok {
101		return nil, fmt.Errorf("no reducer specified for refId %v", rn.RefID)
102	}
103	redFunc, ok := rawReducer.(string)
104	if !ok {
105		return nil, fmt.Errorf("expected reducer to be a string, got %T for refId %v", rawReducer, rn.RefID)
106	}
107
108	return NewReduceCommand(rn.RefID, redFunc, varToReduce), nil
109}
110
111// NeedsVars returns the variable names (refIds) that are dependencies
112// to execute the command and allows the command to fulfill the Command interface.
113func (gr *ReduceCommand) NeedsVars() []string {
114	return []string{gr.VarToReduce}
115}
116
117// Execute runs the command and returns the results or an error if the command
118// failed to execute.
119func (gr *ReduceCommand) Execute(ctx context.Context, vars mathexp.Vars) (mathexp.Results, error) {
120	newRes := mathexp.Results{}
121	for _, val := range vars[gr.VarToReduce].Values {
122		series, ok := val.(mathexp.Series)
123		if !ok {
124			return newRes, fmt.Errorf("can only reduce type series, got type %v", val.Type())
125		}
126		num, err := series.Reduce(gr.refID, gr.Reducer)
127		if err != nil {
128			return newRes, err
129		}
130		newRes.Values = append(newRes.Values, num)
131	}
132	return newRes, nil
133}
134
135// ResampleCommand is an expression command for resampling of a timeseries.
136type ResampleCommand struct {
137	Window        time.Duration
138	VarToResample string
139	Downsampler   string
140	Upsampler     string
141	TimeRange     TimeRange
142	refID         string
143}
144
145// NewResampleCommand creates a new ResampleCMD.
146func NewResampleCommand(refID, rawWindow, varToResample string, downsampler string, upsampler string, tr TimeRange) (*ResampleCommand, error) {
147	// TODO: validate reducer here, before execution
148	window, err := gtime.ParseDuration(rawWindow)
149	if err != nil {
150		return nil, fmt.Errorf(`failed to parse resample "window" duration field %q: %w`, window, err)
151	}
152	return &ResampleCommand{
153		Window:        window,
154		VarToResample: varToResample,
155		Downsampler:   downsampler,
156		Upsampler:     upsampler,
157		TimeRange:     tr,
158		refID:         refID,
159	}, nil
160}
161
162// UnmarshalResampleCommand creates a ResampleCMD from Grafana's frontend query.
163func UnmarshalResampleCommand(rn *rawNode) (*ResampleCommand, error) {
164	rawVar, ok := rn.Query["expression"]
165	if !ok {
166		return nil, fmt.Errorf("no variable to resample for refId %v", rn.RefID)
167	}
168	varToReduce, ok := rawVar.(string)
169	if !ok {
170		return nil, fmt.Errorf("expected resample input variable to be type string, but got type %T for refId %v", rawVar, rn.RefID)
171	}
172	varToReduce = strings.TrimPrefix(varToReduce, "$")
173	varToResample := varToReduce
174
175	rawWindow, ok := rn.Query["window"]
176	if !ok {
177		return nil, fmt.Errorf("no time duration specified for the window in resample command for refId %v", rn.RefID)
178	}
179	window, ok := rawWindow.(string)
180	if !ok {
181		return nil, fmt.Errorf("expected resample window to be a string, got %T for refId %v", rawWindow, rn.RefID)
182	}
183
184	rawDownsampler, ok := rn.Query["downsampler"]
185	if !ok {
186		return nil, fmt.Errorf("no downsampler function specified in resample command for refId %v", rn.RefID)
187	}
188	downsampler, ok := rawDownsampler.(string)
189	if !ok {
190		return nil, fmt.Errorf("expected resample downsampler to be a string, got type %T for refId %v", downsampler, rn.RefID)
191	}
192
193	rawUpsampler, ok := rn.Query["upsampler"]
194	if !ok {
195		return nil, fmt.Errorf("no downsampler specified in resample command for refId %v", rn.RefID)
196	}
197	upsampler, ok := rawUpsampler.(string)
198	if !ok {
199		return nil, fmt.Errorf("expected resample downsampler to be a string, got type %T for refId %v", upsampler, rn.RefID)
200	}
201
202	return NewResampleCommand(rn.RefID, window, varToResample, downsampler, upsampler, rn.TimeRange)
203}
204
205// NeedsVars returns the variable names (refIds) that are dependencies
206// to execute the command and allows the command to fulfill the Command interface.
207func (gr *ResampleCommand) NeedsVars() []string {
208	return []string{gr.VarToResample}
209}
210
211// Execute runs the command and returns the results or an error if the command
212// failed to execute.
213func (gr *ResampleCommand) Execute(ctx context.Context, vars mathexp.Vars) (mathexp.Results, error) {
214	newRes := mathexp.Results{}
215	for _, val := range vars[gr.VarToResample].Values {
216		series, ok := val.(mathexp.Series)
217		if !ok {
218			return newRes, fmt.Errorf("can only resample type series, got type %v", val.Type())
219		}
220		num, err := series.Resample(gr.refID, gr.Window, gr.Downsampler, gr.Upsampler, gr.TimeRange.From, gr.TimeRange.To)
221		if err != nil {
222			return newRes, err
223		}
224		newRes.Values = append(newRes.Values, num)
225	}
226	return newRes, nil
227}
228
229// CommandType is the type of the expression command.
230type CommandType int
231
232const (
233	// TypeUnknown is the CMDType for an unrecognized expression type.
234	TypeUnknown CommandType = iota
235	// TypeMath is the CMDType for a math expression.
236	TypeMath
237	// TypeReduce is the CMDType for a reduction expression.
238	TypeReduce
239	// TypeResample is the CMDType for a resampling expression.
240	TypeResample
241	// TypeClassicConditions is the CMDType for the classic condition operation.
242	TypeClassicConditions
243)
244
245func (gt CommandType) String() string {
246	switch gt {
247	case TypeMath:
248		return "math"
249	case TypeReduce:
250		return "reduce"
251	case TypeResample:
252		return "resample"
253	case TypeClassicConditions:
254		return "classic_conditions"
255	default:
256		return "unknown"
257	}
258}
259
260// ParseCommandType returns a CommandType from its string representation.
261func ParseCommandType(s string) (CommandType, error) {
262	switch s {
263	case "math":
264		return TypeMath, nil
265	case "reduce":
266		return TypeReduce, nil
267	case "resample":
268		return TypeResample, nil
269	case "classic_conditions":
270		return TypeClassicConditions, nil
271	default:
272		return TypeUnknown, fmt.Errorf("'%v' is not a recognized expression type", s)
273	}
274}
275