diff --git a/.gitignore b/.gitignore index 4eeca0d..daf913b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,24 @@ -.vagrant/ -vendor/ -/libucl.dll +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/.travis-ucl.sh b/.travis-ucl.sh new file mode 100755 index 0000000..48e2101 --- /dev/null +++ b/.travis-ucl.sh @@ -0,0 +1,9 @@ +#!/bin/sh +UCL_VERSION=0.7.3 +set -ex +mkdir /tmp/libucl +cd /tmp/libucl +wget https://github.com/vstakhov/libucl/archive/$UCL_VERSION.tar.gz +tar xzf $UCL_VERSION.tar.gz +cd libucl-$UCL_VERSION && ./autogen.sh && ./configure --prefix=/usr --enable-urls --enable-signatures && make && sudo make install +rm -fr /tmp/libucl diff --git a/.travis.yml b/.travis.yml index dfc743f..9c03eb2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,8 @@ +sudo: required language: go go: - - 1.2.1 + - tip -script: make test +before_install: + - ./.travis-ucl.sh diff --git a/Makefile b/Makefile deleted file mode 100644 index e2efabb..0000000 --- a/Makefile +++ /dev/null @@ -1,35 +0,0 @@ -LIBUCL_NAME=libucl.a - -# If we're on Windows, we need to change some variables so things compile -# properly. -ifeq ($(OS), Windows_NT) - LIBUCL_NAME=libucl.dll -endif - -export CGO_CFLAGS CGO_LDFLAGS PATH - -all: libucl - go test - -libucl: vendor/libucl/$(LIBUCL_NAME) - -vendor/libucl/libucl.a: vendor/libucl - cd vendor/libucl && \ - cmake cmake/ && \ - make - -vendor/libucl/libucl.dll: vendor/libucl - cd vendor/libucl && \ - $(MAKE) -f Makefile.w32 && \ - cp .obj/libucl.dll . && \ - cp libucl.dll $(CURDIR) - -vendor/libucl: - rm -rf vendor/libucl - mkdir -p vendor/libucl - git clone https://github.com/vstakhov/libucl.git vendor/libucl - -clean: - rm -rf vendor - -.PHONY: all clean libucl test diff --git a/README.md b/README.md index 7fc356e..6cf4301 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,34 @@ # Libucl Library for Go +[![Build Status](https://travis-ci.org/bitmark-inc/go-libucl.svg?branch=master)](https://travis-ci.org/bitmark-inc/go-libucl) +[![GoDoc](https://godoc.org/github.com/bitmark-inc/go-libucl?status.svg)](https://godoc.org/github.com/bitmark-inc/go-libucl) + +This version of go-libucl is forked from the[mitchellh version](https://github.com/mitchellh/go-libucl), +and [draring version](http://godoc.org/github.com/draringi/go-libucl) +with the goal of having a version with a focus on using the OS's copy of libucl, in a portable manner, +as well as improve the Documentation quality. +As such, it uses pkg-config to determine the location of libucl. go-libucl is a [libucl](https://github.com/vstakhov/libucl) library for [Go](http://golang.org). Rather than re-implement libucl in Go, this library uses cgo to bind directly to libucl. This allows the libucl project to be -the central source of knowledge. This project works on Mac OS X, Linux, and -Windows. +the central source of knowledge. This project has been tested on Linux and FreeBSD. **Warning:** This library is still under development and API compatibility is not guaranteed. Additionally, it is not feature complete yet, though it is certainly usable for real purposes (we do!). -## Installation +## Notes +* macro calling convention changed +* macro callback now gets paramters object in addion to body text -Because we vendor the source of libucl, you can go ahead and get it directly. -We'll keep up to date with libucl. The package name is `libucl`. +## Prerequisites +* libucl (This is a wrapper for this library) +* pkg-config (cgo uses this for locate where libucl is) + +## Installation ``` -$ go get github.com/mitchellh/go-libucl +$ go get github.com/bitmark-inc/go-libucl ``` -Documentation is available on GoDoc: http://godoc.org/github.com/mitchellh/go-libucl - -### Compiling Libucl - -Libucl should compile easily and cleanly on POSIX systems. - -On Windows, msys should be used. msys-regex needs to be compiled. +Documentation is available on GoDoc: http://godoc.org/github.com/bitmark-inc/go-libucl diff --git a/decoder.go b/decoder.go index 6454891..af9164d 100644 --- a/decoder.go +++ b/decoder.go @@ -24,6 +24,12 @@ func decode(name string, o *Object, result reflect.Value) error { return decodeIntoInterface(name, o, result) case reflect.Int: return decodeIntoInt(name, o, result) + case reflect.Int64: + return decodeIntoInt(name, o, result) + case reflect.Uint: + return decodeIntoUint(name, o, result) + case reflect.Uint64: + return decodeIntoUint(name, o, result) case reflect.Map: return decodeIntoMap(name, o, result) case reflect.Ptr: @@ -37,8 +43,6 @@ func decode(name string, o *Object, result reflect.Value) error { default: return fmt.Errorf("%s: unsupported type: %s", name, result.Kind()) } - - return nil } func decodeIntoBool(name string, o *Object, result reflect.Value) error { @@ -73,6 +77,22 @@ func decodeIntoInt(name string, o *Object, result reflect.Value) error { return nil } +func decodeIntoUint(name string, o *Object, result reflect.Value) error { + switch o.Type() { + case ObjectTypeString: + i, err := strconv.ParseUint(o.ToString(), 0, result.Type().Bits()) + if err == nil { + result.SetUint(i) + } else { + return fmt.Errorf("cannot parse '%s' as int: %s", name, err) + } + default: + result.SetUint(o.ToUint()) + } + + return nil +} + func decodeIntoInterface(name string, o *Object, result reflect.Value) error { var set reflect.Value redecode := true @@ -114,12 +134,13 @@ func decodeIntoInterface(name string, o *Object, result reflect.Value) error { for o := outer.Next(); o != nil; o = outer.Next() { m := make(map[string]interface{}) inner := o.Iterate(true) + inner_loop: for o2 := inner.Next(); o2 != nil; o2 = inner.Next() { var raw interface{} err = decode(name, o2, reflect.Indirect(reflect.ValueOf(&raw))) o2.Close() if err != nil { - break + break inner_loop } m[o2.Key()] = raw @@ -139,7 +160,7 @@ func decodeIntoInterface(name string, o *Object, result reflect.Value) error { set = reflect.Indirect(reflect.New(reflect.TypeOf(""))) default: return fmt.Errorf( - "%s: unsupported type to interface: %s", name, o.Type()) + "%s: unsupported type to interface: %v", name, o.Type()) } if redecode { @@ -269,7 +290,7 @@ func decodeIntoString(name string, o *Object, result reflect.Value) error { case ObjectTypeInt: result.SetString(strconv.FormatInt(o.ToInt(), 10)) default: - return fmt.Errorf("%s: unsupported type to string: %s", name, objType) + return fmt.Errorf("%s: unsupported type to string: %v", name, objType) } return nil @@ -290,6 +311,7 @@ func decodeIntoStruct(name string, o *Object, result reflect.Value) error { structs = structs[1:] structType := structVal.Type() + struct_loop: for i := 0; i < structType.NumField(); i++ { fieldType := structType.Field(i) @@ -305,16 +327,17 @@ func decodeIntoStruct(name string, o *Object, result reflect.Value) error { // if specified in the tag. squash := false tagParts := strings.Split(fieldType.Tag.Get(tagName), ",") + tag_loop: for _, tag := range tagParts[1:] { if tag == "squash" { squash = true - break + break tag_loop } } if squash { structs = append(structs, result.FieldByName(fieldType.Name)) - continue + continue struct_loop } } @@ -325,8 +348,9 @@ func decodeIntoStruct(name string, o *Object, result reflect.Value) error { usedKeys := make(map[string]struct{}) decodedFields := make([]string, 0, len(fields)) - decodedFieldsVal := make([]reflect.Value, 0) - unusedKeysVal := make([]reflect.Value, 0) + var decodedFieldsVal []reflect.Value + var unusedKeysVal []reflect.Value +field_loop: for fieldType, field := range fields { if !field.IsValid() { // This should never happen @@ -336,7 +360,7 @@ func decodeIntoStruct(name string, o *Object, result reflect.Value) error { // If we can't set the field, then it is unexported or something, // and we just continue onwards. if !field.CanSet() { - continue + continue field_loop } fieldName := fieldType.Name @@ -347,20 +371,20 @@ func decodeIntoStruct(name string, o *Object, result reflect.Value) error { switch tagParts[1] { case "decodedFields": decodedFieldsVal = append(decodedFieldsVal, field) - continue + continue field_loop case "key": field.SetString(o.Key()) - continue + continue field_loop case "object": // Increase the ref count o.Ref() // Sete the object field.Set(reflect.ValueOf(o)) - continue + continue field_loop case "unusedKeys": unusedKeysVal = append(unusedKeysVal, field) - continue + continue field_loop } } @@ -373,9 +397,10 @@ func decodeIntoStruct(name string, o *Object, result reflect.Value) error { // Do a slower search by iterating over each key and // doing case-insensitive search. iter := o.Iterate(true) + element_loop: for elem = iter.Next(); elem != nil; elem = iter.Next() { if strings.EqualFold(elem.Key(), fieldName) { - break + break element_loop } elem.Close() @@ -384,7 +409,7 @@ func decodeIntoStruct(name string, o *Object, result reflect.Value) error { if elem == nil { // No key matching this field. - continue + continue field_loop } } @@ -402,16 +427,17 @@ func decodeIntoStruct(name string, o *Object, result reflect.Value) error { err = decode(fieldName, elem, field) } else { iter := elem.Iterate(false) + iteration_loop: for { obj := iter.Next() if obj == nil { - break + break iteration_loop } err = decode(fieldName, obj, field) obj.Close() if err != nil { - break + break iteration_loop } } iter.Close() diff --git a/decoder_test.go b/decoder_test.go index 15a03cb..5b56bd6 100644 --- a/decoder_test.go +++ b/decoder_test.go @@ -2,21 +2,38 @@ package libucl import ( "reflect" + "sort" "testing" ) +type foobarSorter []string + +func (s foobarSorter) Len() int { return len(s) } +func (s foobarSorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s foobarSorter) Less(i, j int) bool { return s[i] > s[j] } + func TestObjectDecode_basic(t *testing.T) { type Basic struct { - Bool bool - BoolStr string - Str string - Num int - NumStr int + Bool bool + BoolStr string + Str string + Num int + NumStr int + Card uint + CardStr uint + Num64 int64 + Num64Str int64 + Card64 uint64 + Card64Str uint64 } obj := testParseString(t, ` - bool = true; str = bar; num = 7; numstr = "42"; - boolstr = true; + bool = true; boolstr = true; + str = bar; + num = 7; numstr = "42"; + card = 29; cardstr = "16384"; + num64 = 77; num64str = "4242"; + card64 = 2929; card64str = "524288"; `) defer obj.Close() @@ -26,11 +43,17 @@ func TestObjectDecode_basic(t *testing.T) { } expected := Basic{ - Bool: true, - BoolStr: "true", - Str: "bar", - Num: 7, - NumStr: 42, + Bool: true, + BoolStr: "true", + Str: "bar", + Num: 7, + NumStr: 42, + Card: 29, + CardStr: 16384, + Num64: 77, + Num64Str: 4242, + Card64: 2929, + Card64Str: 524288, } if !reflect.DeepEqual(result, expected) { @@ -162,7 +185,7 @@ func TestObjectDecode_mapObject(t *testing.T) { expected := map[string]interface{}{ "foo": "bar", "bar": []map[string]interface{}{ - map[string]interface{}{ + { "baz": "what", }, }, @@ -189,10 +212,10 @@ func TestObjectDecode_mapObjectMultiple(t *testing.T) { expected := map[string]interface{}{ "foo": "bar", "bar": []map[string]interface{}{ - map[string]interface{}{ + { "baz": "what", }, - map[string]interface{}{ + { "port": 3000, }, }, @@ -226,7 +249,7 @@ func TestObjectDecode_mapReuseVal(t *testing.T) { expected := Result{ Struct: map[string]Struct{ - "foo": Struct{ + "foo": { Foo: "bar", Bar: "baz", }, @@ -376,14 +399,14 @@ func TestObjectDecode_structKeys(t *testing.T) { if err := obj.Decode(&result); err != nil { t.Fatalf("err: %s", err) } - + sort.Sort(foobarSorter(result.Keys)) expected := Struct{ Foo: []string{"foo", "bar", "12"}, Bar: "baz", Keys: []string{"Foo", "Bar"}, } if !reflect.DeepEqual(expected, result) { - t.Fatalf("bad: %#v", result) + t.Fatalf("bad: %#v, expected: %#v", result, expected) } } @@ -405,7 +428,7 @@ func TestObjectDecode_mapStructNamed(t *testing.T) { } expected := map[string]Nested{ - "foo": Nested{ + "foo": { Name: "foo", Foo: "bar", }, @@ -441,7 +464,7 @@ func TestObjectDecode_mapStructObject(t *testing.T) { defer result.Value["foo"].Object.Close() expected := map[string]Nested{ - "foo": Nested{ + "foo": { Foo: "bar", Object: fooObj, }, diff --git a/go-libucl.h b/go-libucl.h index 2ca1d1d..ff82f84 100644 --- a/go-libucl.h +++ b/go-libucl.h @@ -3,28 +3,33 @@ #include #include +#include static inline char *_go_uchar_to_char(const unsigned char *c) { return (char *)c; } +static inline unsigned char *_go_char_to_uchar(const char *c) { + return (unsigned char *)c; +} + //------------------------------------------------------------------- // Helpers: Macros //------------------------------------------------------------------- // This is declared in parser.go and invokes the Go function callback for // a specific macro (specified by the ID). -extern bool go_macro_call(int, char *data, int); +extern bool go_macro_call(int idx, ucl_object_t *arguments, char *data, int length); // Indirection that actually calls the Go macro handler. -static inline bool _go_macro_handler(const unsigned char *data, size_t len, void* ud) { - return go_macro_call((int)ud, (char*)data, (int)len); +static inline bool _go_macro_handler(const unsigned char *data, size_t len, const ucl_object_t *arguments, void* ud) { + return go_macro_call((int)ud, (ucl_object_t *)arguments, (char*)data, (int)len); } // Returns the ucl_macro_handler that we have, since we can't get this // type from cgo. static inline ucl_macro_handler _go_macro_handler_func() { - return &_go_macro_handler; + return (ucl_macro_handler)&_go_macro_handler; } // This just converts an int to a void*, because Go doesn't let us do that @@ -33,4 +38,6 @@ static inline void *_go_macro_index(int idx) { return (void *)idx; } +typedef struct ucl_schema_error ucl_schema_error_t; + #endif /* _GOLIBUCL_H_INCLUDED */ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3c7f987 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/bitmark-inc/go-libucl + +go 1.12 diff --git a/libucl.go b/libucl.go index 6542301..e4e3d66 100644 --- a/libucl.go +++ b/libucl.go @@ -1,5 +1,7 @@ +// Package libucl provides golang bindings to libucl, a configuration library for +// UCL, the Universal Configuration Language. package libucl -// #cgo CFLAGS: -Ivendor/libucl/include -Wno-int-to-void-pointer-cast -// #cgo LDFLAGS: -Lvendor/libucl -lucl +// #cgo CFLAGS: -Wno-int-to-void-pointer-cast +// #cgo pkg-config: libucl import "C" diff --git a/object.go b/object.go index 7513be1..dee0a95 100644 --- a/object.go +++ b/object.go @@ -21,29 +21,44 @@ type ObjectIter struct { type ObjectType int const ( + // ObjectTypeObject signifies a UCL Object (key/value pair) ObjectTypeObject ObjectType = iota + // ObjectTypeArray signifies a UCL array ObjectTypeArray + // ObjectTypeInt signifies an integer number ObjectTypeInt + // ObjectTypeFloat signifies a floating-point nmber ObjectTypeFloat + // ObjectTypeString signifies a string ObjectTypeString + // ObjectTypeBoolean signifies a boolean value (true/false) ObjectTypeBoolean + // ObjectTypeTime signifies time in seconds stored as a floating-point number ObjectTypeTime + // ObjectTypeUserData signifies an opaque user-provided pointer, typically + // used in macros ObjectTypeUserData + // ObjectTypeNull signifies a null/non-existant value ObjectTypeNull ) // Emitter is a type of built-in emitter that can be used to convert -// an object to another config format. +// an object to another config format. All Emitters except EmitConfig +// are considered lossy, and information such as implicit arrays can be lost. type Emitter int const ( + // EmitJSON is the canonic json notation (with spaces indented structure) EmitJSON Emitter = iota + // EmitJSONCompact is compact json notation (without spaces or newlines) EmitJSONCompact + // EmitConfig is UCL (nginx-like) EmitConfig + // EmitYAML is yaml inlined notation EmitYAML ) -// Free the memory associated with the object. This must be called when +// Close frees the memory associated with the object. This must be called when // you're done using it. func (o *Object) Close() error { C.ucl_object_unref(o.object) @@ -69,6 +84,7 @@ func (o *Object) Delete(key string) { C.ucl_object_delete_key(o.object, ckey) } +// Get returns the element with matching key. func (o *Object) Get(key string) *Object { ckey := C.CString(key) defer C.free(unsafe.Pointer(ckey)) @@ -83,7 +99,7 @@ func (o *Object) Get(key string) *Object { return result } -// Iterate over the objects in this object. +// Iterate returns an iterator that iterates over the objects in this object. // // The iterator must be closed when it is finished. // @@ -99,7 +115,7 @@ func (o *Object) Iterate(expand bool) *ObjectIter { } } -// Returns the key of this value/object as a string, or the empty +// Key returns the key of this value/object as a string, or the empty // string if the object doesn't have a key. func (o *Object) Key() string { return C.GoString(C.ucl_object_key(o.object)) @@ -118,10 +134,10 @@ func (o *Object) Len() uint { iter := o.Iterate(false) defer iter.Close() - var count uint = 0 + var count uint for obj := iter.Next(); obj != nil; obj = iter.Next() { obj.Close() - count += 1 + count++ } return count @@ -130,14 +146,14 @@ func (o *Object) Len() uint { return uint(o.object.len) } -// Increments the ref count associated with this. You have to call +// Ref increments the ref count associated with this. You have to call // close an additional time to free the memory. func (o *Object) Ref() error { C.ucl_object_ref(o.object) return nil } -// Returns the type that this object represents. +// Type returns the type that this object represents. func (o *Object) Type() ObjectType { return ObjectType(C.ucl_object_type(o.object)) } @@ -146,26 +162,37 @@ func (o *Object) Type() ObjectType { // Conversion Functions //------------------------------------------------------------------------ +// ToBool converts a UCL Object to a boolean value func (o *Object) ToBool() bool { return bool(C.ucl_object_toboolean(o.object)) } +// ToInt converts a UCL Object to a signed integer value func (o *Object) ToInt() int64 { return int64(C.ucl_object_toint(o.object)) } +// ToUint converts a UCL Object to an unsigned integer value +func (o *Object) ToUint() uint64 { + return uint64(C.ucl_object_toint(o.object)) +} + +// ToFloat converts a UCL Object to an floating point value func (o *Object) ToFloat() float64 { return float64(C.ucl_object_todouble(o.object)) } +// ToString converts a UCL Object to a string func (o *Object) ToString() string { return C.GoString(C.ucl_object_tostring(o.object)) } +// Close frees the object iterator func (o *ObjectIter) Close() { C.ucl_object_unref(o.object) } +// Next returns the next iterative UCL Object func (o *ObjectIter) Next() *Object { obj := C.ucl_iterate_object(o.object, &o.iter, C._Bool(o.expand)) if obj == nil { @@ -177,3 +204,66 @@ func (o *ObjectIter) Next() *Object { return &Object{object: obj} } + +// StringFlag are flags used in the conversion of strings into UCL objects +type StringFlag int + +const ( + // StringEscape tells the converter to JSON escape the inputed string + StringEscape StringFlag = C.UCL_STRING_ESCAPE + // StringTrim tells the converter to trim leading and trailing whitespaces + StringTrim StringFlag = C.UCL_STRING_TRIM + // StringParseBoolean tells the converter to parse the inputted string as a boolean + StringParseBoolean StringFlag = C.UCL_STRING_PARSE_BOOLEAN + // StringParseInt tells the converter to parse the inputted string as an integer + StringParseInt StringFlag = C.UCL_STRING_PARSE_INT + // StringParseDouble tells the converter to parse the inputted string as a + // floating-point number + StringParseDouble StringFlag = C.UCL_STRING_PARSE_DOUBLE + // StringParseTime tells the converter to parse the inputted string as a + // time value, and treat as a floating-point number. + StringParseTime StringFlag = C.UCL_STRING_PARSE_TIME + // StringParseNumber tells the converter to parse the inputted string as a + // number (integer, floating-point or time) + StringParseNumber StringFlag = C.UCL_STRING_PARSE_TIME + // StringParse tells the converter to parse the inputted string + StringParse StringFlag = C.UCL_STRING_PARSE + // StringParseBytes tells the converter to parse the inputted string as being + // in bytes notation (e.g. 10k = 10*1024, not 10*1000) + StringParseBytes StringFlag = C.UCL_STRING_PARSE_BYTES +) + +// NewObject creates a new UCL Object from a string, JSON escaping it in the process +func NewObject(data string) *Object { + cData := C.CString(data) + defer C.free(unsafe.Pointer(cData)) + obj := C.ucl_object_fromlstring(cData, C.size_t(len(data))) + return &Object{object: obj} +} + +// NewFormattedObject creates a new UCL Object from a string, according to the instructions +// given in the flags +func NewFormattedObject(data string, flags StringFlag) *Object { + cData := C.CString(data) + defer C.free(unsafe.Pointer(cData)) + obj := C.ucl_object_fromstring_common(cData, C.size_t(len(data)), uint32(flags)) + return &Object{object: obj} +} + +// NewIntegerObject creates a new UCL Object from a 64-bit integer +func NewIntegerObject(data int64) *Object { + obj := C.ucl_object_fromint(C.int64_t(data)) + return &Object{object: obj} +} + +// NewDoubleObject creates a new UCL Object from a 64-bit floating-point number +func NewDoubleObject(data float64) *Object { + obj := C.ucl_object_fromdouble(C.double(data)) + return &Object{object: obj} +} + +// NewBoolObject creates a new UCL Object from a boolean +func NewBoolObject(data bool) *Object { + obj := C.ucl_object_frombool(C.bool(data)) + return &Object{object: obj} +} diff --git a/object_test.go b/object_test.go index df4456f..edb02fb 100644 --- a/object_test.go +++ b/object_test.go @@ -234,3 +234,97 @@ func TestObjectToFloat_negativeOneThird(t *testing.T) { t.Fatalf("bad: %#v, expected: %v", v.ToFloat(), g) } } + +func TestIntToObject(t *testing.T) { + var testValue int64 = 42 + obj := NewIntegerObject(testValue) + defer obj.Close() + v := obj.ToInt() + if v != testValue { + t.Fatalf("bad: %d, expected: %d", v, testValue) + } +} + +func TestBoolToObject(t *testing.T) { + objTrue := NewBoolObject(true) + defer objTrue.Close() + if !objTrue.ToBool() { + t.Fatalf("bad: false, expected: true") + } + objFalse := NewBoolObject(false) + defer objFalse.Close() + if objFalse.ToBool() { + t.Fatalf("bad: true, expected: false") + } +} + +func TestFloatToObject(t *testing.T) { + testValue := -1.0 / 3.0 + obj := NewDoubleObject(testValue) + defer obj.Close() + if obj.ToFloat() != testValue { + t.Fatalf("bad: %#v, expected: %v", obj.ToFloat(), testValue) + } +} + +func TestSimpleStringToObject(t *testing.T) { + s := "Hello World!" + obj := NewObject(s) + defer obj.Close() + if obj.ToString() != s { + t.Fatalf("bad: \"%s\", expected: \"%s\"", obj.ToString(), s) + } +} + +func TestJSONEscapeStringToObject(t *testing.T) { + inputString := "complex\tstring\nfrom\"hell\"" + escapedString := "complex\\tstring\\nfrom\\\"hell\\\"" + obj := NewObject(inputString) + defer obj.Close() + if obj.ToString() != escapedString { + t.Fatalf("bad: \"%s\", expected: \"%s\"", obj.ToString(), escapedString) + } +} + +func TestStringIntToObject(t *testing.T) { + var i int64 = 42 + s := "42" + obj := NewFormattedObject(s, StringParseInt) + defer obj.Close() + if obj.ToInt() != i { + t.Fatalf("bad: %d, expected: %d", obj.ToInt(), i) + } +} + +func TestStringFloatToObject(t *testing.T) { + expectedValue := -1.0 / 3.0 + testString := "-0.3333333333333333148296162562473909929394721984863281" + obj := NewFormattedObject(testString, StringParseDouble) + defer obj.Close() + if obj.ToFloat() != expectedValue { + t.Fatalf("bad: %#v, expected: %v", obj.ToFloat(), expectedValue) + } +} + +func TestStringBoolToObject(t *testing.T) { + objFalse := NewFormattedObject("false", StringParseBoolean) + defer objFalse.Close() + if objFalse.ToBool() { + t.Fatal("bad: true, expected: false") + } + objTrue := NewFormattedObject("true", StringParseBoolean) + defer objTrue.Close() + if !objTrue.ToBool() { + t.Fatal("bad: false, expected: true") + } +} + +func TestStringTrimToObject(t *testing.T) { + rawString := " Hello World\n\t\n\t\n\t " + expectedResult := "Hello World" + obj := NewFormattedObject(rawString, StringTrim) + defer obj.Close() + if obj.ToString() != expectedResult { + t.Fatalf("bad: \"%s\", expected: \"%s\"", obj.ToString(), expectedResult) + } +} diff --git a/parser.go b/parser.go index bdf3e02..0dcc297 100644 --- a/parser.go +++ b/parser.go @@ -2,6 +2,7 @@ package libucl import ( "errors" + "os" "sync" "unsafe" ) @@ -10,24 +11,33 @@ import ( import "C" // MacroFunc is the callback type for macros. -type MacroFunc func(string) - -// ParserFlag are flags that can be used to initialize a parser. +// return true os string is valid +// a macro call looks like: // -// ParserKeyLowercase will lowercase all keys. +// .macro(UCL-OBJECT) "body-text" +// .macro(key=value) "body-text" +// .macro(params={key1=value1;key2=value2}) "body" // -// ParserKeyZeroCopy will attempt to do a zero-copy parse if possible. +type MacroFunc func(args Object, body string) bool + +// ParserFlag are flags that can be used to initialize a parser. type ParserFlag int const ( + // ParserKeyLowercase will lowercase all keys. ParserKeyLowercase ParserFlag = C.UCL_PARSER_KEY_LOWERCASE - ParserZeroCopy = C.UCL_PARSER_ZEROCOPY - ParserNoTime = C.UCL_PARSER_NO_TIME + // ParserZeroCopy will attempt to do a zero-copy parse if possible. + ParserZeroCopy ParserFlag = C.UCL_PARSER_ZEROCOPY + // ParserNoTime will treat time values as strings. + ParserNoTime ParserFlag = C.UCL_PARSER_NO_TIME + // ParserNoImplicitArrays forces the creation explicit arrays instead of + // implicit ones + ParserNoImplicitArrays ParserFlag = C.UCL_PARSER_NO_IMPLICIT_ARRAYS ) // Keeps track of all the macros internally -var macros map[int]MacroFunc = nil -var macrosIdx int = 0 +var macros map[int]MacroFunc +var macrosIdx int var macrosLock sync.Mutex // Parser is responsible for parsing libucl data. @@ -80,8 +90,8 @@ func (p *Parser) AddFile(path string) error { return nil } -// Closes the parser. Once it is closed it can no longer be used. You -// should always close the parser once you're done with it to clean up +// Close frees the parser. Once it is freed it can no longer be used. You +// should always free the parser once you're done with it to clean up // any unused memory. func (p *Parser) Close() { C.ucl_parser_free(p.parser) @@ -95,7 +105,7 @@ func (p *Parser) Close() { } } -// Retrieves the root-level object for a configuration. +// Object retrieves the root-level object for a configuration. func (p *Parser) Object() *Object { obj := C.ucl_parser_get_object(p.parser) if obj == nil { @@ -134,17 +144,73 @@ func (p *Parser) RegisterMacro(name string, f MacroFunc) { } //export go_macro_call -func go_macro_call(id C.int, data *C.char, n C.int) C.bool { +func go_macro_call(id C.int, arguments *C.ucl_object_t, data *C.char, n C.int) C.bool { macrosLock.Lock() f := macros[int(id)] macrosLock.Unlock() + args := Object{ + object: arguments, + } + // Macro not found, return error if f == nil { return false } // Macro found, call it! - f(C.GoStringN(data, n)) + f(args, C.GoStringN(data, n)) return true } + +// SetFileVariables sets the standard file variables ($FILENAME and $CURDIR) based +// on the provided filepath. If the argument expand is true, the path will be expanded +// out to an absolute path +// +// For example, if the current directory is /etc/nginx, and you give a path of +// ../file.conf, with exand = false, $FILENAME = ../file.conf and $CURDIR = .., +// while with expand = true, $FILENAME = /etc/file.conf and $CURDIR = /etc +func (p *Parser) SetFileVariables(filepath string, expand bool) error { + cpath := C.CString(filepath) + defer C.free(unsafe.Pointer(cpath)) + result := C.ucl_parser_set_filevars(p.parser, cpath, C.bool(expand)) + if !result { + errstr := C.ucl_parser_get_error(p.parser) + return errors.New(C.GoString(errstr)) + } + return nil +} + +// RegisterVariable adds a new variable to the parser, which can be accessed in +// the configuration file as $variable_name +func (p *Parser) RegisterVariable(variable, value string) { + cVariable := C.CString(variable) + defer C.free(unsafe.Pointer(cVariable)) + cValue := C.CString(value) + defer C.free(unsafe.Pointer(cValue)) + C.ucl_parser_register_variable(p.parser, cVariable, cValue) +} + +// AddFileAndSetVariables is a combination of AddFile and SetFileVariables. +// It is meant to be a simple way to do both actions in a single function call. +func (p *Parser) AddFileAndSetVariables(path string, expand bool) error { + err := p.AddFile(path) + if err != nil { + return err + } + + err = p.SetFileVariables(path, expand) + return err +} + +// AddOpenFile reads in the configuration from a file already opened using os.Open +// or a related function. +func (p *Parser) AddOpenFile(f *os.File) error { + fd := f.Fd() + result := C.ucl_parser_add_fd(p.parser, C.int(fd)) + if !result { + errstr := C.ucl_parser_get_error(p.parser) + return errors.New(C.GoString(errstr)) + } + return nil +} diff --git a/parser_test.go b/parser_test.go index ff59f52..ad18edc 100644 --- a/parser_test.go +++ b/parser_test.go @@ -2,6 +2,7 @@ package libucl import ( "io/ioutil" + "path" "testing" ) @@ -97,11 +98,18 @@ func TestParserAddFile(t *testing.T) { func TestParserRegisterMacro(t *testing.T) { value := "" - macro := func(data string) { - value = data + parameter := "" + macro := func(args Object, body string) bool { + thing := args.Get("thing") + if nil == thing { + return false + } + parameter = thing.ToString() + value = body + return true } - config := `.foo "bar";` + config := `.foo(thing=something) "bar";` p := NewParser(0) defer p.Close() @@ -113,7 +121,10 @@ func TestParserRegisterMacro(t *testing.T) { } if value != "bar" { - t.Fatalf("bad: %#v", value) + t.Fatalf("body bad: %#v", value) + } + if parameter != "something" { + t.Fatalf("parameter bad: %#v", parameter) } } @@ -131,3 +142,69 @@ func TestParseString(t *testing.T) { t.Fatalf("bad: %d", obj.Len()) } } + +func TestRegisterVariable(t *testing.T) { + p := NewParser(0) + defer p.Close() + value := "bar" + p.RegisterVariable("FOO", value) + + err := p.AddString("foo = $FOO; baz = boo;") + if err != nil { + t.Fatalf("err: %s", err) + } + obj := p.Object() + if obj == nil { + t.Fatal("Configuration should produce object") + } + defer obj.Close() + + v := obj.Get("foo") + if v == nil { + t.Fatal("Key \"foo\" should exist") + } + defer v.Close() + if v.ToString() != value { + t.Fatalf("bad: \"%s\", expected: \"%s\"", v.ToString(), value) + } +} + +func TestFileVariables(t *testing.T) { + tf, err := ioutil.TempFile("", "libucl") + if err != nil { + t.Fatalf("err: %s", err) + } + tf.Write([]byte("file = $FILENAME; dir = $CURDIR")) + tf.Close() + + p := NewParser(0) + defer p.Close() + + if err := p.AddFileAndSetVariables(tf.Name(), false); err != nil { + t.Fatalf("err: %s", err) + } + + obj := p.Object() + if obj == nil { + t.Fatal("obj should not be nil") + } + defer obj.Close() + + filename := obj.Get("file") + if filename == nil { + t.Fatal("key \"file\" should exist") + } + defer filename.Close() + if filename.ToString() != tf.Name() { + t.Errorf("bad: %s, expected %s", filename.ToString(), tf.Name()) + } + + dir := obj.Get("dir") + if dir == nil { + t.Fatal("key \"dir\" should exist") + } + defer dir.Close() + if dir.ToString() != path.Dir(tf.Name()) { + t.Errorf("bad: %s, expected %s", dir.ToString(), path.Dir(tf.Name())) + } +} diff --git a/validator.go b/validator.go new file mode 100644 index 0000000..742c9c5 --- /dev/null +++ b/validator.go @@ -0,0 +1,74 @@ +package libucl + +import "errors" + +// #include "go-libucl.h" +import "C" + +// SchemaErrorCode is a progamatic way to ficgure out what went wrong during validation +type SchemaErrorCode int + +const ( + // SchemaOK means nothing went wrong (no error) + SchemaOK SchemaErrorCode = iota + // SchemaTypeMismatch means the type of object is wrong + SchemaTypeMismatch + // SchemaInvalidSchema means the provided schema is not valid according to + // json-schema draft 4 + SchemaInvalidSchema + // SchemaMissingProperty means at least one property of the object is missing + SchemaMissingProperty + // SchemaConstraint means a contraint was not met. + SchemaConstraint + // SchemaMissingDependency means a dependency was not met + SchemaMissingDependency + // SchemaGenericError is a generic error (matches UCL_SCHEMA_UNKNOWN in libucl) + SchemaGenericError +) + +// SchemaError contains information on an error found when validating an UCL Object +// against a provided json-schema style schema +type SchemaError struct { + code SchemaErrorCode + message string + object *Object +} + +// Validate validates the object againt a provided schema, which should conform to +// the 4th draft of the json-schema standard +func (o *Object) Validate(schema *Object) (SchemaError, error) { + var cError C.ucl_schema_error_t + var err error + var schemaError SchemaError + ok := C.ucl_object_validate(schema.object, o.object, &cError) + if !ok { + schemaError.code = SchemaErrorCode(cError.code) + schemaError.message = bufferToString(cError.msg) + schemaError.object = &Object{object: cError.obj} + err = errors.New(schemaError.message) + } + return schemaError, err +} + +// Convert a fixed char array to a go string, as gGo doesn't let us use +// [128]C.char as *C.char in C.GoString +func bufferToString(buffer [128]C.char) string { + var byteBuffer []byte + for _, c := range buffer { + if c == 0 { + break + } else { + byteBuffer = append(byteBuffer, byte(c)) + } + } + return string(byteBuffer) +} + +// Purely exists for the test, as cgo isn't supported inside tests +func byteToBufferAdapter(buffer [128]byte) string { + var tmp [128]C.char + for i, c := range buffer { + tmp[i] = C.char(c) + } + return bufferToString(tmp) +} diff --git a/validator_test.go b/validator_test.go new file mode 100644 index 0000000..2adb98f --- /dev/null +++ b/validator_test.go @@ -0,0 +1,14 @@ +package libucl + +import ( + "testing" +) + +func TestBufferToString(t *testing.T) { + expected := "Hello World!" + buffer := [128]byte{'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd', '!', 0} + str := byteToBufferAdapter(buffer) + if str != expected { + t.Fatalf("bad: \"%s\", expected: \"%s\"", str, expected) + } +}