ops.go 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. // The ops package provides a set of functions that can be used to
  2. // build a filter AST programatically using basic functions, versus
  3. // building a filter AST leveraging all structural components of the
  4. // tree.
  5. package ops
  6. import (
  7. "fmt"
  8. "reflect"
  9. "strings"
  10. "github.com/opencost/opencost/pkg/filter21/allocation"
  11. "github.com/opencost/opencost/pkg/filter21/ast"
  12. "github.com/opencost/opencost/pkg/util/typeutil"
  13. )
  14. // keyFieldType is used to extract field, key, and field type
  15. type keyFieldType interface {
  16. Field() string
  17. Key() string
  18. Type() string
  19. }
  20. // This is somewhat of a fancy solution, but allows us to "register" DefaultFieldByName funcs
  21. // funcs by Field type.
  22. var defaultFieldByType = map[string]any{
  23. // typeutil.TypeOf[cloud.CloudAggregationField](): cloud.DefaultFieldByName,
  24. typeutil.TypeOf[allocation.AllocationField](): allocation.DefaultFieldByName,
  25. // typeutil.TypeOf[asset.AssetField](): asset.DefaultFieldByName,
  26. // typeutil.TypeOf[containerstats.ContainerStatsField](): containerstats.DefaultFieldByName,
  27. }
  28. // asField looks up a specific T field instance by name and returns the default
  29. // ast.Field value for that type.
  30. func asField[T ~string](field T) *ast.Field {
  31. lookup, ok := defaultFieldByType[typeutil.TypeOf[T]()]
  32. if !ok {
  33. return nil
  34. }
  35. defaultLookup, ok := lookup.(func(T) *ast.Field)
  36. if !ok {
  37. return nil
  38. }
  39. return defaultLookup(field)
  40. }
  41. // asFieldWithType allows for a field to be looked up by name and type.
  42. func asFieldWithType(field string, typ string) *ast.Field {
  43. lookup, ok := defaultFieldByType[typ]
  44. if !ok {
  45. return nil
  46. }
  47. // This is the sacrifice being made to allow a simple filter
  48. // builder style API. In the cases where we have keys, the typical
  49. // field type gets wrapped in a KeyedFieldType, which is a string
  50. // that holds all the parameterized data, but no way to get back from
  51. // string to T-instance.
  52. // Since we have the type name, we can use that to lookup the specific
  53. // func(T) *ast.Field function to be used.
  54. funcType := reflect.TypeOf(lookup)
  55. // Assert that the function has a single parameter (type T)
  56. if funcType.NumIn() != 1 {
  57. return nil
  58. }
  59. // Get a reference to the first parameter's type (T)
  60. inType := funcType.In(0)
  61. // Create a reflect.Value for the string field, then convert it to
  62. // the T type from the function's parameter list. (This has to be
  63. // done to ensure we're executing the call with the correct types)
  64. fieldParam := reflect.ValueOf(field).Convert(inType)
  65. // Create a reflect.Value for the lookup function
  66. callable := reflect.ValueOf(lookup)
  67. // Call the function with the fieldParam value, and get the result
  68. result := callable.Call([]reflect.Value{fieldParam})
  69. if len(result) == 0 {
  70. return nil
  71. }
  72. // Lastly, extract the value from the reflect.Value and ensure we can
  73. // cast it to *ast.Field
  74. resultValue := result[0].Interface()
  75. if f, ok := resultValue.(*ast.Field); ok {
  76. return f
  77. }
  78. return nil
  79. }
  80. // KeyedFieldType is a type alias for field is a special field type that can
  81. // be deconstructed into multiple components.
  82. type KeyedFieldType string
  83. func (k KeyedFieldType) Field() string {
  84. str := string(k)
  85. idx := strings.Index(str, "$")
  86. if idx == -1 {
  87. return ""
  88. }
  89. return str[0:idx]
  90. }
  91. func (k KeyedFieldType) Key() string {
  92. str := string(k)
  93. idx := strings.Index(str, "$")
  94. if idx == -1 {
  95. return ""
  96. }
  97. lastIndex := strings.LastIndex(str, "$")
  98. if lastIndex == -1 {
  99. return ""
  100. }
  101. return str[idx+1 : lastIndex]
  102. }
  103. func (k KeyedFieldType) Type() string {
  104. str := string(k)
  105. lastIndex := strings.LastIndex(str, "$")
  106. if lastIndex == -1 {
  107. return ""
  108. }
  109. return str[lastIndex+1:]
  110. }
  111. func WithKey[T ~string](field T, key string) KeyedFieldType {
  112. k := fmt.Sprintf("%s$%s$%s", field, key, typeutil.TypeOf[T]())
  113. return KeyedFieldType(k)
  114. }
  115. func toFieldAndKey[T ~string](field T) (*ast.Field, string) {
  116. var inner any = field
  117. if kft, ok := inner.(keyFieldType); ok {
  118. return asFieldWithType(kft.Field(), kft.Type()), kft.Key()
  119. }
  120. return asField(field), ""
  121. }
  122. func identifier[T ~string](field T) ast.Identifier {
  123. f, key := toFieldAndKey(field)
  124. return ast.Identifier{
  125. Field: f,
  126. Key: key,
  127. }
  128. }
  129. func And(node, next ast.FilterNode, others ...ast.FilterNode) ast.FilterNode {
  130. operands := append([]ast.FilterNode{node, next}, others...)
  131. return &ast.AndOp{
  132. Operands: operands,
  133. }
  134. }
  135. func Or(node, next ast.FilterNode, others ...ast.FilterNode) ast.FilterNode {
  136. operands := append([]ast.FilterNode{node, next}, others...)
  137. return &ast.OrOp{
  138. Operands: operands,
  139. }
  140. }
  141. func Not(node ast.FilterNode) ast.FilterNode {
  142. return &ast.NotOp{
  143. Operand: node,
  144. }
  145. }
  146. func Eq[T ~string](field T, value string) ast.FilterNode {
  147. return &ast.EqualOp{
  148. Left: identifier(field),
  149. Right: value,
  150. }
  151. }
  152. func NotEq[T ~string](field T, value string) ast.FilterNode {
  153. return Not(Eq(field, value))
  154. }
  155. func Contains[T ~string](field T, value string) ast.FilterNode {
  156. return &ast.ContainsOp{
  157. Left: identifier(field),
  158. Right: value,
  159. }
  160. }
  161. func NotContains[T ~string](field T, value string) ast.FilterNode {
  162. return Not(Contains(field, value))
  163. }
  164. func ContainsPrefix[T ~string](field T, value string) ast.FilterNode {
  165. return &ast.ContainsPrefixOp{
  166. Left: identifier(field),
  167. Right: value,
  168. }
  169. }
  170. func NotContainsPrefix[T ~string](field T, value string) ast.FilterNode {
  171. return Not(ContainsPrefix(field, value))
  172. }
  173. func ContainsSuffix[T ~string](field T, value string) ast.FilterNode {
  174. return &ast.ContainsSuffixOp{
  175. Left: identifier(field),
  176. Right: value,
  177. }
  178. }
  179. func NotContainsSuffix[T ~string](field T, value string) ast.FilterNode {
  180. return Not(ContainsSuffix(field, value))
  181. }