Browse Source

Added two standard FieldNameMappers. Closes #152.

Dmitry Panov 5 năm trước cách đây
mục cha
commit
f1752421c4
4 tập tin đã thay đổi với 122 bổ sung0 xóa
  1. 20 0
      README.md
  2. 57 0
      object_goreflect.go
  3. 41 0
      object_goreflect_test.go
  4. 4 0
      parser/lexer.go

+ 20 - 0
README.md

@@ -137,6 +137,26 @@ A JS value can be exported into its default Go representation using Value.Export
 
 Alternatively it can be exported into a specific Go variable using Runtime.ExportTo() method.
 
+Mapping struct field and method names
+-------------------------------------
+By default, the names are passed through as is which means they are capitalised. This does not match
+the standard JavaScript naming convention, so if you need to make your JS code look more natural or if you are
+dealing with a 3rd party library, you can use a FieldNameMapper:
+
+```go
+vm := New()
+vm.SetFieldNameMapper(TagFieldNameMapper("json", true))
+type S struct {
+    Field int `json:"field"`
+}
+vm.Set("s", S{Field: 42})
+res, _ := vm.RunString(`s.field`) // without the mapper it would have been s.Field
+fmt.Println(res.Export())
+// Output: 42
+```
+
+There are two standard mappers: `TagFieldNameMapper` and `UncapFieldNameMapper`, or you can use your own implementation.
+
 Native Constructors
 -------------------
 

+ 57 - 0
object_goreflect.go

@@ -2,8 +2,10 @@ package goja
 
 import (
 	"fmt"
+	"github.com/dop251/goja/parser"
 	"go/ast"
 	"reflect"
+	"strings"
 )
 
 // JsonEncodable allows custom JSON encoding by JSON.stringify()
@@ -23,6 +25,44 @@ type FieldNameMapper interface {
 	MethodName(t reflect.Type, m reflect.Method) string
 }
 
+type tagFieldNameMapper struct {
+	tagName      string
+	uncapMethods bool
+}
+
+func (tfm tagFieldNameMapper) FieldName(_ reflect.Type, f reflect.StructField) string {
+	tag := f.Tag.Get(tfm.tagName)
+	if idx := strings.IndexByte(tag, ','); idx != -1 {
+		tag = tag[:idx]
+	}
+	if parser.IsIdentifier(tag) {
+		return tag
+	}
+	return ""
+}
+
+func uncapitalize(s string) string {
+	return strings.ToLower(s[0:1]) + s[1:]
+}
+
+func (tfm tagFieldNameMapper) MethodName(_ reflect.Type, m reflect.Method) string {
+	if tfm.uncapMethods {
+		return uncapitalize(m.Name)
+	}
+	return m.Name
+}
+
+type uncapFieldNameMapper struct {
+}
+
+func (u uncapFieldNameMapper) FieldName(_ reflect.Type, f reflect.StructField) string {
+	return uncapitalize(f.Name)
+}
+
+func (u uncapFieldNameMapper) MethodName(_ reflect.Type, m reflect.Method) string {
+	return uncapitalize(m.Name)
+}
+
 type reflectFieldInfo struct {
 	Index     []int
 	Anonymous bool
@@ -512,3 +552,20 @@ func (r *Runtime) SetFieldNameMapper(mapper FieldNameMapper) {
 	r.fieldNameMapper = mapper
 	r.typeInfoCache = nil
 }
+
+// TagFieldNameMapper returns a FieldNameMapper that uses the given tagName for struct fields and optionally
+// uncapitalises (making the first letter lower case) method names.
+// The common tag value syntax is supported (name[,options]), however options are ignored.
+// Setting name to anything other than a valid ECMAScript identifier makes the field hidden.
+func TagFieldNameMapper(tagName string, uncapMethods bool) FieldNameMapper {
+	return tagFieldNameMapper{
+		tagName:      tagName,
+		uncapMethods: uncapMethods,
+	}
+}
+
+// UncapFieldNameMapper returns a FieldNameMapper that uncapitalises struct field and method names
+// making the first letter lower case.
+func UncapFieldNameMapper() FieldNameMapper {
+	return uncapFieldNameMapper{}
+}

+ 41 - 0
object_goreflect_test.go

@@ -1,6 +1,7 @@
 package goja
 
 import (
+	"fmt"
 	"reflect"
 	"strings"
 	"testing"
@@ -950,3 +951,43 @@ func TestStructNonAddressableAnonStruct(t *testing.T) {
 	}
 
 }
+
+func TestTagFieldNameMapperInvalidId(t *testing.T) {
+	vm := New()
+	vm.SetFieldNameMapper(TagFieldNameMapper("json", true))
+	type S struct {
+		Field int `json:"-"`
+	}
+	vm.Set("s", S{Field: 42})
+	res, err := vm.RunString(`s.hasOwnProperty("field") || s.hasOwnProperty("Field")`)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if res != valueFalse {
+		t.Fatalf("Unexpected result: %v", res)
+	}
+}
+
+func ExampleTagFieldNameMapper() {
+	vm := New()
+	vm.SetFieldNameMapper(TagFieldNameMapper("json", true))
+	type S struct {
+		Field int `json:"field"`
+	}
+	vm.Set("s", S{Field: 42})
+	res, _ := vm.RunString(`s.field`)
+	fmt.Println(res.Export())
+	// Output: 42
+}
+
+func ExampleUncapFieldNameMapper() {
+	vm := New()
+	s := testGoReflectMethod_O{
+		Test: "passed",
+	}
+	vm.SetFieldNameMapper(UncapFieldNameMapper())
+	vm.Set("s", s)
+	res, _ := vm.RunString(`s.test + " and " + s.method("passed too")`)
+	fmt.Println(res.Export())
+	// Output: passed and passed too
+}

+ 4 - 0
parser/lexer.go

@@ -26,6 +26,10 @@ func isDecimalDigit(chr rune) bool {
 	return '0' <= chr && chr <= '9'
 }
 
+func IsIdentifier(s string) bool {
+	return matchIdentifier.MatchString(s)
+}
+
 func digitValue(chr rune) int {
 	switch {
 	case '0' <= chr && chr <= '9':