-
Notifications
You must be signed in to change notification settings - Fork 23
Expand file tree
/
Copy pathclass_reflect.go
More file actions
505 lines (417 loc) · 15.9 KB
/
class_reflect.go
File metadata and controls
505 lines (417 loc) · 15.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
package quickjs
import (
"errors"
"fmt"
"reflect"
"slices"
"strings"
)
// =============================================================================
// LEVEL 1: REFLECTION-BASED AUTO BINDING
// =============================================================================
// ReflectOptions configures automatic class binding behavior
type ReflectOptions struct {
// MethodPrefix filters methods by prefix (empty = all methods)
MethodPrefix string
// IgnoredMethods lists method names to skip during binding
IgnoredMethods []string
// IgnoredFields lists field names to skip during binding
IgnoredFields []string
}
// ReflectOption configures ReflectOptions using functional options pattern
type ReflectOption func(*ReflectOptions)
// WithMethodPrefix filters methods by name prefix
func WithMethodPrefix(prefix string) ReflectOption {
return func(opts *ReflectOptions) {
opts.MethodPrefix = prefix
}
}
// WithIgnoredMethods specifies method names to skip during binding
func WithIgnoredMethods(methods ...string) ReflectOption {
return func(opts *ReflectOptions) {
opts.IgnoredMethods = append(opts.IgnoredMethods, methods...)
}
}
// WithIgnoredFields specifies field names to skip during binding
func WithIgnoredFields(fields ...string) ReflectOption {
return func(opts *ReflectOptions) {
opts.IgnoredFields = append(opts.IgnoredFields, fields...)
}
}
// BindClass automatically creates and builds a JavaScript class from a Go struct type using reflection.
// This is a convenience method that combines BindClassBuilder and Build.
// Only exported fields and methods are bound to maintain Go encapsulation principles.
//
// Example usage:
//
// constructor, classID, err := ctx.BindClass(&MyStruct{})
// if err != nil { return err }
// ctx.Globals().Set("MyStruct", constructor)
func (ctx *Context) BindClass(structType interface{}, options ...ReflectOption) (*Value, uint32) {
builder, err := ctx.BindClassBuilder(structType, options...)
if err != nil {
return ctx.ThrowError(err), 0
}
return builder.Build(ctx)
}
// BindClassBuilder automatically creates a ClassBuilder from a Go struct type using reflection.
// Returns a ClassBuilder that can be further customized using chain methods before Build().
// Only exported fields and methods are analyzed to maintain Go encapsulation principles.
//
// Example usage:
//
// builder, err := ctx.BindClassBuilder(&MyStruct{})
// if err != nil { return err }
// constructor, classID, err := builder.Build(ctx)
//
// // Or with additional customization:
// builder, err := ctx.BindClassBuilder(&MyStruct{})
// if err != nil { return err }
// constructor, classID, err := builder.
// StaticMethod("Create", myCreateFunc).
// ReadOnlyAccessor("version", myVersionGetter).
// Build(ctx)
func (ctx *Context) BindClassBuilder(structType interface{}, options ...ReflectOption) (*ClassBuilder, error) {
// Parse options with defaults (zero values are appropriate defaults)
opts := &ReflectOptions{}
for _, option := range options {
option(opts)
}
// Extract reflect.Type from input
typ, err := getReflectType(structType)
if err != nil {
return nil, err
}
// Determine class name from type
className := typ.Name()
if className == "" {
return nil, errors.New("cannot determine class name from anonymous type")
}
// Build ClassBuilder using reflection analysis
return buildClassFromReflection(ctx, className, typ, opts)
}
// getReflectType extracts reflect.Type from various input types
func getReflectType(structType interface{}) (reflect.Type, error) {
switch v := structType.(type) {
case reflect.Type:
// Direct reflect.Type input
if v.Kind() != reflect.Struct {
return nil, errors.New("type must be a struct type")
}
return v, nil
default:
// Interface{} input - analyze the actual value
typ := reflect.TypeOf(v)
if typ == nil {
return nil, errors.New("cannot get type from nil value")
}
// Handle pointer to struct
if typ.Kind() == reflect.Ptr {
if typ.Elem().Kind() != reflect.Struct {
return nil, errors.New("value must be a struct or pointer to struct")
}
return typ.Elem(), nil
}
// Handle direct struct value
if typ.Kind() != reflect.Struct {
return nil, errors.New("value must be a struct or pointer to struct")
}
return typ, nil
}
}
// buildClassFromReflection creates a ClassBuilder from reflection analysis
// MODIFIED FOR SCHEME C: Constructor signature changed to receive instance and return Go object
func buildClassFromReflection(ctx *Context, className string, typ reflect.Type, opts *ReflectOptions) (*ClassBuilder, error) {
builder := NewClassBuilder(className)
// SCHEME C: Modified constructor with new signature
// Constructor now receives pre-created instance and returns Go object to associate
builder.Constructor(func(ctx *Context, instance *Value, args []*Value) (interface{}, error) {
// Create new Go object instance
goObject := reflect.New(typ).Interface()
// Initialize from constructor arguments using mixed parameter strategy
if len(args) > 0 {
if err := initializeFromArgs(goObject, args, typ, ctx); err != nil {
return nil, fmt.Errorf("constructor initialization failed: %w", err)
}
}
// SCHEME C: Return Go object for automatic association with instance
// The framework handles accessor-based synchronization automatically
return goObject, nil
})
// Add accessors (only exported fields) - unchanged
addReflectionAccessors(builder, typ, opts)
// Add methods (only exported methods) - unchanged
addReflectionMethods(builder, typ, opts)
return builder, nil
}
// initializeFromArgs implements mixed parameter constructor strategy
func initializeFromArgs(instance interface{}, args []*Value, typ reflect.Type, ctx *Context) error {
// Smart strategy: single object argument uses named parameters
if len(args) == 1 && args[0].IsObject() && !args[0].IsArray() {
return initializeFromObjectArgs(instance, args[0], typ, ctx)
}
// Otherwise use positional parameters
return initializeFromPositionalArgs(instance, args, typ, ctx)
}
// initializeFromPositionalArgs initializes struct fields from positional arguments
func initializeFromPositionalArgs(instance interface{}, args []*Value, typ reflect.Type, ctx *Context) error {
val := reflect.ValueOf(instance).Elem() // Dereference pointer to get struct value
argIndex := 0
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
// Skip unexported fields
if !field.IsExported() {
continue
}
// Break if no more arguments
if argIndex >= len(args) {
break
}
fieldValue := val.Field(i)
if fieldValue.CanSet() {
if err := ctx.unmarshal(args[argIndex], fieldValue); err != nil {
return fmt.Errorf("failed to set field %s: %w", field.Name, err)
}
argIndex++
}
}
return nil
}
// initializeFromObjectArgs initializes struct fields from object properties (named parameters)
func initializeFromObjectArgs(instance interface{}, obj *Value, typ reflect.Type, ctx *Context) error {
val := reflect.ValueOf(instance).Elem()
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
// Skip unexported fields
if !field.IsExported() {
continue
}
// Use unified tag parsing from marshal.go
tagInfo := parseFieldTag(field)
if tagInfo.Skip {
continue // Field marked with "-" tag
}
// Check if object has this property and set field value
if obj.Has(tagInfo.Name) {
propValue := obj.Get(tagInfo.Name)
defer propValue.Free()
fieldValue := val.Field(i)
if fieldValue.CanSet() {
if err := ctx.unmarshal(propValue, fieldValue); err != nil {
return fmt.Errorf("failed to set field %s from property %s: %w", field.Name, tagInfo.Name, err)
}
}
}
}
return nil
}
// addReflectionMethods scans struct methods and adds them to the ClassBuilder
// Only exported methods are processed due to Go reflection limitations
func addReflectionMethods(builder *ClassBuilder, typ reflect.Type, opts *ReflectOptions) {
// Get pointer type to include pointer receiver methods
ptrTyp := reflect.PointerTo(typ)
for i := 0; i < ptrTyp.NumMethod(); i++ {
method := ptrTyp.Method(i)
if shouldSkipMethod(method, opts) {
continue
}
// Create method wrapper and add to builder
methodWrapper := createMethodWrapper(method)
builder.Method(method.Name, methodWrapper)
}
}
// addReflectionAccessors scans struct fields and adds them as accessors
// Only exported fields are processed to maintain Go encapsulation principles
func addReflectionAccessors(builder *ClassBuilder, typ reflect.Type, opts *ReflectOptions) {
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
if shouldSkipField(field, opts) {
continue
}
// Use unified tag parsing from marshal.go
tagInfo := parseFieldTag(field)
if tagInfo.Skip {
continue // Field marked with "-" tag
}
// Create getter and setter functions
getter := createFieldGetter(field, i)
setter := createFieldSetter(field, i)
// Add accessor to builder (instance accessors by default)
builder.Accessor(tagInfo.Name, getter, setter)
}
}
// =============================================================================
// OPTIMIZED HELPER FUNCTIONS
// =============================================================================
// getValidObjectValue extracts and validates object value from JavaScript instance
func getValidObjectValue(ctx *Context, this *Value) (reflect.Value, error) {
obj, err := this.GetGoObject()
if err != nil {
return reflect.Value{}, fmt.Errorf("failed to get instance data: %w", err)
}
objValue := reflect.ValueOf(obj)
if objValue.Kind() == reflect.Ptr {
objValue = objValue.Elem()
}
return objValue, nil
}
// shouldSkipMethod determines if a method should be skipped during binding
func shouldSkipMethod(method reflect.Method, opts *ReflectOptions) bool {
// Check prefix filter
if opts.MethodPrefix != "" && !strings.HasPrefix(method.Name, opts.MethodPrefix) {
return true
}
// Check ignored list
if slices.Contains(opts.IgnoredMethods, method.Name) {
return true
}
// Check special methods
return isSpecialMethod(method.Name)
}
// shouldSkipField determines if a field should be skipped during binding
func shouldSkipField(field reflect.StructField, opts *ReflectOptions) bool {
// Only exported fields are bound to maintain Go encapsulation principles
if !field.IsExported() {
return true
}
// Check ignored list
return slices.Contains(opts.IgnoredFields, field.Name)
}
// createMethodWrapper creates a ClassMethodFunc wrapper around a reflect.Method
func createMethodWrapper(method reflect.Method) ClassMethodFunc {
return func(ctx *Context, this *Value, args []*Value) *Value {
objValue, err := getValidObjectValue(ctx, this)
if err != nil {
return ctx.ThrowError(err)
}
// Ensure we have a pointer receiver if the method requires it
if objValue.Kind() != reflect.Ptr && method.Type.In(0).Kind() == reflect.Ptr {
// In our implementation, objValue is always addressable because it comes
// from reflect.New().Elem(), so we can safely take its address
objValue = objValue.Addr()
}
// Get the method value
methodValue := objValue.Method(method.Index)
// Convert JavaScript arguments to Go method arguments
methodArgs, err := convertJSArgsToMethodArgs(&method, args, ctx)
if err != nil {
return ctx.ThrowError(fmt.Errorf("failed to prepare method arguments: %w", err))
}
// Call the method
results := methodValue.Call(methodArgs)
// Convert results back to JavaScript values
return convertMethodResults(results, ctx)
}
}
// createFieldGetter creates a getter function for a struct field
func createFieldGetter(field reflect.StructField, fieldIndex int) ClassGetterFunc {
return func(ctx *Context, this *Value) *Value {
objValue, err := getValidObjectValue(ctx, this)
if err != nil {
return ctx.ThrowError(err)
}
// In our implementation, fieldIndex is always valid since it comes from
// the loop in addReflectionAccessors, and fieldValue is always valid
// for exported fields that we bind
fieldValue := objValue.Field(fieldIndex)
jsValue, err := ctx.Marshal(fieldValue.Interface())
if err != nil {
return ctx.ThrowError(fmt.Errorf("failed to marshal field %s: %w", field.Name, err))
}
return jsValue
}
}
// createFieldSetter creates a setter function for a struct field
func createFieldSetter(field reflect.StructField, fieldIndex int) ClassSetterFunc {
return func(ctx *Context, this *Value, value *Value) *Value {
objValue, err := getValidObjectValue(ctx, this)
if err != nil {
return ctx.ThrowError(err)
}
// In our implementation, fieldIndex is always valid and fieldValue
// is always settable for exported fields that we bind
fieldValue := objValue.Field(fieldIndex)
// Use Unmarshal to convert JavaScript value to Go value
tempVar := reflect.New(fieldValue.Type())
if err := ctx.Unmarshal(value, tempVar.Interface()); err != nil {
return ctx.ThrowError(fmt.Errorf("failed to unmarshal value for field %s: %w", field.Name, err))
}
// Set the field value
fieldValue.Set(tempVar.Elem())
// Return the new JavaScript value
// Note: In practice, if a JavaScript value can be successfully unmarshaled to a Go field,
// that field value can almost always be marshaled back to JavaScript. The error case
// is extremely rare and would indicate a deeper issue with the marshal/unmarshal system.
returnValue, _ := ctx.Marshal(fieldValue.Interface())
return returnValue
}
}
// convertJSArgsToMethodArgs converts JavaScript arguments to Go reflect.Values for method calls
func convertJSArgsToMethodArgs(method *reflect.Method, args []*Value, ctx *Context) ([]reflect.Value, error) {
methodType := method.Type
numIn := methodType.NumIn()
// First argument is the receiver, so we skip it
numArgs := numIn - 1
if len(args) > numArgs {
return nil, fmt.Errorf("too many arguments: expected %d, got %d", numArgs, len(args))
}
// Prepare argument slice
reflectArgs := make([]reflect.Value, numArgs)
for i := 0; i < numArgs; i++ {
argType := methodType.In(i + 1) // +1 to skip receiver
if i < len(args) {
// Convert JavaScript value to Go value using marshal.go logic
argValue := reflect.New(argType).Elem()
if err := ctx.unmarshal(args[i], argValue); err != nil {
return nil, fmt.Errorf("failed to convert argument %d: %w", i, err)
}
reflectArgs[i] = argValue
} else {
// Use zero value for missing arguments
reflectArgs[i] = reflect.Zero(argType)
}
}
return reflectArgs, nil
}
// convertMethodResults converts method return values to JavaScript value
func convertMethodResults(results []reflect.Value, ctx *Context) *Value {
switch len(results) {
case 0:
return ctx.Undefined()
case 1:
jsValue, err := ctx.marshal(results[0])
if err != nil {
return ctx.ThrowError(fmt.Errorf("failed to marshal return value: %w", err))
}
return jsValue
default:
// Multiple return values - create an array
returnArray := make([]interface{}, len(results))
for i, result := range results {
returnArray[i] = result.Interface()
}
jsValue, err := ctx.Marshal(returnArray)
if err != nil {
return ctx.ThrowError(fmt.Errorf("failed to marshal return values: %w", err))
}
return jsValue
}
}
// =============================================================================
// UTILITY FUNCTIONS
// =============================================================================
// specialMethods contains method names that should be skipped during reflection binding
// Using map for O(1) lookup performance instead of slice
var specialMethods = map[string]bool{
"String": true, // String() string - handled specially for debugging
"Error": true, // Error() string - for error interface
"GoString": true, // GoString() string - for debugging
"Format": true, // Format() - for fmt package
"Finalize": true, // Finalize() - our cleanup interface
"MarshalJS": true, // MarshalJS() - our marshal interface
"UnmarshalJS": true, // UnmarshalJS() - our unmarshal interface
}
// isSpecialMethod checks if a method name should be skipped during reflection binding
func isSpecialMethod(name string) bool {
return specialMethods[name]
}