|
| 1 | +package graphql |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "fmt" |
| 6 | + "reflect" |
| 7 | + |
| 8 | + "github.com/graph-gophers/graphql-go/ast" |
| 9 | + "github.com/graph-gophers/graphql-go/errors" |
| 10 | + "github.com/graph-gophers/graphql-go/internal/exec/packer" |
| 11 | + "github.com/graph-gophers/graphql-go/internal/exec/selected" |
| 12 | +) |
| 13 | + |
| 14 | +type directiveArgsCacheKey struct { |
| 15 | + def *ast.DirectiveDefinition |
| 16 | + typ reflect.Type |
| 17 | +} |
| 18 | + |
| 19 | +type DirectiveContext struct { |
| 20 | + owner *Schema |
| 21 | + schema *ast.Schema |
| 22 | + directive *ast.Directive |
| 23 | + args map[string]any |
| 24 | + fieldArgs map[string]any |
| 25 | + decodedArgs map[reflect.Type]reflect.Value |
| 26 | +} |
| 27 | + |
| 28 | +func (c DirectiveContext) DecodeArgs(dst any) error { |
| 29 | + if c.schema == nil || c.directive == nil { |
| 30 | + return fmt.Errorf("directive context is missing schema metadata") |
| 31 | + } |
| 32 | + def := c.schema.Directives[c.directive.Name.Name] |
| 33 | + if def == nil { |
| 34 | + return fmt.Errorf("directive %q is not defined in the schema", c.directive.Name.Name) |
| 35 | + } |
| 36 | + if dst == nil { |
| 37 | + return fmt.Errorf("destination must be a non-nil pointer") |
| 38 | + } |
| 39 | + typ := reflect.TypeOf(dst) |
| 40 | + if typ.Kind() != reflect.Pointer { |
| 41 | + return fmt.Errorf("destination must be a pointer, got %s", typ) |
| 42 | + } |
| 43 | + rv := reflect.ValueOf(dst) |
| 44 | + if rv.IsNil() { |
| 45 | + return fmt.Errorf("destination must be a non-nil pointer") |
| 46 | + } |
| 47 | + var ( |
| 48 | + sp *packer.StructPacker |
| 49 | + err error |
| 50 | + ) |
| 51 | + if c.owner != nil { |
| 52 | + sp, err = c.owner.directiveArgsPacker(def, typ) |
| 53 | + } else { |
| 54 | + b := packer.NewBuilder() |
| 55 | + sp, err = b.MakeStructPacker(def.Arguments, typ) |
| 56 | + if err != nil { |
| 57 | + return err |
| 58 | + } |
| 59 | + err = b.Finish() |
| 60 | + } |
| 61 | + if err != nil { |
| 62 | + return err |
| 63 | + } |
| 64 | + if c.decodedArgs != nil { |
| 65 | + if packed, ok := c.decodedArgs[typ]; ok { |
| 66 | + rv.Elem().Set(packed.Elem()) |
| 67 | + return nil |
| 68 | + } |
| 69 | + } |
| 70 | + packed, err := sp.Pack(c.args) |
| 71 | + if err != nil { |
| 72 | + return err |
| 73 | + } |
| 74 | + if c.decodedArgs != nil { |
| 75 | + c.decodedArgs[typ] = packed |
| 76 | + } |
| 77 | + rv.Elem().Set(packed.Elem()) |
| 78 | + return nil |
| 79 | +} |
| 80 | + |
| 81 | +func (c DirectiveContext) FieldArg(name string, dst any) error { |
| 82 | + if c.fieldArgs == nil { |
| 83 | + return fmt.Errorf("directive context is missing field arguments") |
| 84 | + } |
| 85 | + if dst == nil { |
| 86 | + return fmt.Errorf("destination must be a non-nil pointer") |
| 87 | + } |
| 88 | + typ := reflect.TypeOf(dst) |
| 89 | + if typ.Kind() != reflect.Pointer { |
| 90 | + return fmt.Errorf("destination must be a pointer, got %s", typ) |
| 91 | + } |
| 92 | + rv := reflect.ValueOf(dst) |
| 93 | + if rv.IsNil() { |
| 94 | + return fmt.Errorf("destination must be a non-nil pointer") |
| 95 | + } |
| 96 | + |
| 97 | + value, ok := c.fieldArgs[name] |
| 98 | + if !ok { |
| 99 | + return fmt.Errorf("field argument %q not found", name) |
| 100 | + } |
| 101 | + |
| 102 | + packed, err := (&packer.ValuePacker{ValueType: typ.Elem()}).Pack(value) |
| 103 | + if err != nil { |
| 104 | + return err |
| 105 | + } |
| 106 | + rv.Elem().Set(packed) |
| 107 | + return nil |
| 108 | +} |
| 109 | + |
| 110 | +// DirectiveVisitor defines the interface for directive visitors. |
| 111 | +type DirectiveVisitor interface { |
| 112 | + // Name returns the name of the directive this visitor handles. |
| 113 | + Name() string |
| 114 | + // Visit is called when the directive is encountered during field definition traversal. |
| 115 | + // Use [DirectiveContext.DecodeArgs] to parse the directive arguments. |
| 116 | + Visit(ctx context.Context, d DirectiveContext) error |
| 117 | +} |
| 118 | + |
| 119 | +// DirectiveVisitors registers one or more directive visitors with the schema. |
| 120 | +// Visitor names must be unique within a schema. |
| 121 | +func DirectiveVisitors(visitors ...DirectiveVisitor) SchemaOpt { |
| 122 | + return func(s *Schema) { |
| 123 | + for _, v := range visitors { |
| 124 | + visitor, err := newDirectiveVisitor(v) |
| 125 | + if err != nil { |
| 126 | + s.optErr = err |
| 127 | + return |
| 128 | + } |
| 129 | + s.directiveVisitors = append(s.directiveVisitors, visitor) |
| 130 | + } |
| 131 | + } |
| 132 | +} |
| 133 | + |
| 134 | +func newDirectiveVisitor(visitor DirectiveVisitor) (DirectiveVisitor, error) { |
| 135 | + if visitor == nil { |
| 136 | + return nil, fmt.Errorf("directive visitor is nil") |
| 137 | + } |
| 138 | + |
| 139 | + name := visitor.Name() |
| 140 | + if name == "" { |
| 141 | + return nil, fmt.Errorf("directive visitor must have a non-empty name") |
| 142 | + } |
| 143 | + |
| 144 | + return visitor, nil |
| 145 | +} |
| 146 | + |
| 147 | +func (s *Schema) validateDirectiveVisitors() error { |
| 148 | + seen := make(map[string]struct{}, len(s.directiveVisitors)) |
| 149 | + for _, v := range s.directiveVisitors { |
| 150 | + name := v.Name() |
| 151 | + def := s.schema.Directives[name] |
| 152 | + if def == nil { |
| 153 | + return fmt.Errorf("directive %q is not defined in the schema", name) |
| 154 | + } |
| 155 | + if _, ok := seen[name]; ok { |
| 156 | + return fmt.Errorf("directive visitor %q is already registered", name) |
| 157 | + } |
| 158 | + seen[name] = struct{}{} |
| 159 | + } |
| 160 | + |
| 161 | + return nil |
| 162 | +} |
| 163 | + |
| 164 | +func directiveArgs(schema *ast.Schema, d *ast.Directive, vars map[string]any) (map[string]any, error) { |
| 165 | + if d == nil { |
| 166 | + return nil, fmt.Errorf("directive is nil") |
| 167 | + } |
| 168 | + def := schema.Directives[d.Name.Name] |
| 169 | + if def == nil { |
| 170 | + return nil, fmt.Errorf("directive %q is not defined in the schema", d.Name.Name) |
| 171 | + } |
| 172 | + args := make(map[string]any, len(def.Arguments)) |
| 173 | + for _, arg := range def.Arguments { |
| 174 | + if v, ok := d.Arguments.Get(arg.Name.Name); ok { |
| 175 | + if isNilValue(v) { |
| 176 | + args[arg.Name.Name] = nil |
| 177 | + continue |
| 178 | + } |
| 179 | + args[arg.Name.Name] = v.Deserialize(vars) |
| 180 | + continue |
| 181 | + } |
| 182 | + if arg.Default != nil { |
| 183 | + args[arg.Name.Name] = arg.Default.Deserialize(nil) |
| 184 | + continue |
| 185 | + } |
| 186 | + args[arg.Name.Name] = nil |
| 187 | + } |
| 188 | + return args, nil |
| 189 | +} |
| 190 | + |
| 191 | +func isNilValue(v ast.Value) bool { |
| 192 | + if v == nil { |
| 193 | + return true |
| 194 | + } |
| 195 | + |
| 196 | + rv := reflect.ValueOf(v) |
| 197 | + switch rv.Kind() { |
| 198 | + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice: |
| 199 | + return rv.IsNil() |
| 200 | + default: |
| 201 | + return false |
| 202 | + } |
| 203 | +} |
| 204 | + |
| 205 | +func (s *Schema) directiveArgsPacker(def *ast.DirectiveDefinition, typ reflect.Type) (*packer.StructPacker, error) { |
| 206 | + if def == nil { |
| 207 | + return nil, fmt.Errorf("directive definition is nil") |
| 208 | + } |
| 209 | + key := directiveArgsCacheKey{def: def, typ: typ} |
| 210 | + |
| 211 | + s.directiveArgsMu.RLock() |
| 212 | + sp := s.directiveArgsPackers[key] |
| 213 | + s.directiveArgsMu.RUnlock() |
| 214 | + if sp != nil { |
| 215 | + return sp, nil |
| 216 | + } |
| 217 | + |
| 218 | + b := packer.NewBuilder() |
| 219 | + sp, err := b.MakeStructPacker(def.Arguments, typ) |
| 220 | + if err != nil { |
| 221 | + return nil, err |
| 222 | + } |
| 223 | + if err := b.Finish(); err != nil { |
| 224 | + return nil, err |
| 225 | + } |
| 226 | + |
| 227 | + s.directiveArgsMu.Lock() |
| 228 | + defer s.directiveArgsMu.Unlock() |
| 229 | + if s.directiveArgsPackers == nil { |
| 230 | + s.directiveArgsPackers = make(map[directiveArgsCacheKey]*packer.StructPacker) |
| 231 | + } |
| 232 | + if cached := s.directiveArgsPackers[key]; cached != nil { |
| 233 | + return cached, nil |
| 234 | + } |
| 235 | + s.directiveArgsPackers[key] = sp |
| 236 | + return sp, nil |
| 237 | +} |
| 238 | + |
| 239 | +func (s *Schema) buildDirectiveCaches() { |
| 240 | + if s.directiveArgsPackers == nil { |
| 241 | + s.directiveArgsPackers = make(map[directiveArgsCacheKey]*packer.StructPacker) |
| 242 | + } |
| 243 | + s.directiveVisitorsByName = make(map[string]DirectiveVisitor, len(s.directiveVisitors)) |
| 244 | + for _, v := range s.directiveVisitors { |
| 245 | + name := v.Name() |
| 246 | + s.directiveVisitorsByName[name] = v |
| 247 | + } |
| 248 | +} |
| 249 | + |
| 250 | +func (s *Schema) runDirectiveVisitors(ctx context.Context, vars map[string]any, sels []selected.Selection) []*errors.QueryError { |
| 251 | + if len(s.directiveVisitorsByName) == 0 { |
| 252 | + return nil |
| 253 | + } |
| 254 | + |
| 255 | + var errs []*errors.QueryError |
| 256 | + |
| 257 | + path := make([]any, 0, 8) |
| 258 | + var walk func([]selected.Selection) |
| 259 | + walk = func(selections []selected.Selection) { |
| 260 | + for _, sel := range selections { |
| 261 | + switch sel := sel.(type) { |
| 262 | + case *selected.SchemaField: |
| 263 | + path = append(path, sel.Alias) |
| 264 | + for _, d := range sel.Directives { |
| 265 | + hook, ok := s.directiveVisitorsByName[d.Name.Name] |
| 266 | + if !ok { |
| 267 | + continue |
| 268 | + } |
| 269 | + args, err := directiveArgs(s.schema, d, vars) |
| 270 | + if err != nil { |
| 271 | + errs = append(errs, &errors.QueryError{Message: err.Error(), Path: append([]any(nil), path...)}) |
| 272 | + continue |
| 273 | + } |
| 274 | + dctx := DirectiveContext{ |
| 275 | + owner: s, |
| 276 | + schema: s.schema, |
| 277 | + fieldArgs: sel.Args, |
| 278 | + directive: d, |
| 279 | + args: args, |
| 280 | + } |
| 281 | + err = hook.Visit(ctx, dctx) |
| 282 | + if err != nil { |
| 283 | + errs = append(errs, &errors.QueryError{Message: err.Error(), Path: append([]any(nil), path...)}) |
| 284 | + } |
| 285 | + } |
| 286 | + walk(sel.Sels) |
| 287 | + path = path[:len(path)-1] |
| 288 | + case *selected.TypeAssertion: |
| 289 | + walk(sel.Sels) |
| 290 | + } |
| 291 | + } |
| 292 | + } |
| 293 | + |
| 294 | + walk(sels) |
| 295 | + if len(errs) != 0 { |
| 296 | + return errs |
| 297 | + } |
| 298 | + return nil |
| 299 | +} |
0 commit comments