You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@qpid.apache.org by ac...@apache.org on 2015/03/03 06:28:46 UTC

[2/2] qpid-proton git commit: PROTON-827: go binding - unmarshalling AMQP string, symbol and binary types into Go types.

PROTON-827: go binding - unmarshalling AMQP string, symbol and binary types into Go types.


Project: http://git-wip-us.apache.org/repos/asf/qpid-proton/repo
Commit: http://git-wip-us.apache.org/repos/asf/qpid-proton/commit/84bea240
Tree: http://git-wip-us.apache.org/repos/asf/qpid-proton/tree/84bea240
Diff: http://git-wip-us.apache.org/repos/asf/qpid-proton/diff/84bea240

Branch: refs/heads/master
Commit: 84bea2403648379ae4da84601662e923282044bb
Parents: 5d8030f
Author: Alan Conway <ac...@redhat.com>
Authored: Mon Mar 2 10:10:14 2015 -0500
Committer: Alan Conway <ac...@redhat.com>
Committed: Tue Mar 3 00:27:52 2015 -0500

----------------------------------------------------------------------
 proton-c/bindings/go/README.md                  |  35 +++
 .../go/src/apache.org/proton/encoding.go        | 216 +++++++++++++++++++
 .../bindings/go/src/apache.org/proton/error.go  |  23 +-
 .../go/src/apache.org/proton/interop_test.go    | 121 +++++++++++
 .../bindings/go/src/apache.org/proton/types.go  |  25 +++
 .../bindings/go/src/apache.org/proton/url.go    |   2 +-
 6 files changed, 414 insertions(+), 8 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/qpid-proton/blob/84bea240/proton-c/bindings/go/README.md
----------------------------------------------------------------------
diff --git a/proton-c/bindings/go/README.md b/proton-c/bindings/go/README.md
index 5ae8ca0..31bdb5d 100644
--- a/proton-c/bindings/go/README.md
+++ b/proton-c/bindings/go/README.md
@@ -94,5 +94,40 @@ also supports traditional locking, so we could adopt locking strategies similar
 to our other bindings, but we should investigate the Go-like alternatives. There
 are analogies between Go channels and AMQP links that we will probably exploit.
 
+## Implementation status
 
+Working on API to marshal/unmarshal AMQP data into Go types.
+
+The API will follow the style of the standard libraries encoding/json and encoding/xml.
+
+To be done:
+
+Easy unmarshaling into native Go types:
+
+- String-like AMQP types (symbol, binary, string) into Go string or []byte
+- Numeric AMQP types into any Go numeric (numeric conversion)
+- Any AMQP type into GO reflect.Value choosing the closest native Go type
+- AMQP map into go struct if keys match struct field names and values match field types
+- AMQP maps into map[K]T if all AMQP keys/values can convert to K and T (reflect.Value allowed)
+- AMQP list into []T if all list elements can convert to T (reflect.Value allowed)
+
+Easy marshaling of native Go types:
+
+- Go struct: amqp map with field names as string keys (use tags to customize?)
+- Go string to AMQP string, Go []byte to AMQP binary, Go numerics to closest AMQP numeric
+- Go []T to AMQP list
+- Go map to AMQP map
+
+Customization:
+
+- Standard encoding libraries define Marshaler and Unmarshaler interfaces.
+- User implements to customize behavior of a user type.
+- Does this require exposing the proton codec?
+
+Exact (strict) (un)marshaling:
+
+- Define special Go AMQP types that exactly reflect AMQP types & encodings.
+- Unmarshal to special types only if exact match for wire
+- Marshal special types exactly
+- Define AMQPValue which can unmarshal from any AMQP type using strict unmarshaling types.
 

http://git-wip-us.apache.org/repos/asf/qpid-proton/blob/84bea240/proton-c/bindings/go/src/apache.org/proton/encoding.go
----------------------------------------------------------------------
diff --git a/proton-c/bindings/go/src/apache.org/proton/encoding.go b/proton-c/bindings/go/src/apache.org/proton/encoding.go
new file mode 100644
index 0000000..eb42c8c
--- /dev/null
+++ b/proton-c/bindings/go/src/apache.org/proton/encoding.go
@@ -0,0 +1,216 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+*/
+
+package proton
+
+//#include <proton/codec.h>
+import "C"
+
+import (
+	"bytes"
+	"io"
+	"reflect"
+	"unsafe"
+)
+
+//
+// Decoding from a pn_data_t
+//
+
+type pnDecoder struct{ data *C.pn_data_t }
+
+func newPnDecoder() pnDecoder { return pnDecoder{C.pn_data(0)} }
+func (pd pnDecoder) free()    { C.pn_data_free((*C.pn_data_t)(pd.data)) }
+
+// decode from bytes. Return bytes decoded and errEOS if we run out of data.
+func (pd pnDecoder) decode(bytes []byte) (n int, err error) {
+	C.pn_data_clear(pd.data)
+	if len(bytes) == 0 {
+		return 0, errEOS
+	}
+	cBuf := (*C.char)(unsafe.Pointer(&bytes[0]))
+	result := int(C.pn_data_decode(pd.data, cBuf, C.size_t(len(bytes))))
+	if result < 0 {
+		return 0, errorCode(result)
+	} else {
+		return result, nil
+	}
+}
+
+// Unmarshal decodes bytes and converts into the value pointed to by v.
+//
+// Returns the number of bytes decoded and an errorCode on error.
+func (pd pnDecoder) unmarshal(bytes []byte, v interface{}) (n int, err error) {
+	n, err = pd.decode(bytes)
+	if err != nil {
+		return
+	}
+	switch v := v.(type) {
+	case *string:
+		err = pd.unmarshalString(v)
+	case *[]byte:
+		err = pd.unmarshalBytes(v)
+	case *Symbol:
+		err = pd.unmarshalSymbol(v)
+	default:
+		note := ""
+		if reflect.TypeOf(v).Kind() != reflect.Ptr {
+			note = "is not a pointer"
+		}
+		return 0, errorf("Unmarshal bad type: %T %s", v, note)
+		// FIXME aconway 2015-03-02: not finished
+	}
+	if err != nil {
+		return 0, err
+	}
+	return
+}
+
+func (pd pnDecoder) unmarshalPnBytes(target string) (pnBytes C.pn_bytes_t, err error) {
+	switch amqpType := C.pn_data_type(pd.data); amqpType {
+	case C.PN_STRING:
+		pnBytes = C.pn_data_get_string(pd.data)
+	case C.PN_BINARY:
+		pnBytes = C.pn_data_get_binary(pd.data)
+	case C.PN_SYMBOL:
+		pnBytes = C.pn_data_get_symbol(pd.data)
+	default:
+		// FIXME aconway 2015-03-02: error message - json style UnmarsalTypeError?
+		return C.pn_bytes_t{}, errorf("Unmarshal cannot convert %#v to %s", amqpType, target)
+	}
+	return pnBytes, nil
+}
+
+func (pd pnDecoder) unmarshalString(v *string) error {
+	pnBytes, err := pd.unmarshalPnBytes("string")
+	if err == nil {
+		*v = C.GoStringN(pnBytes.start, C.int(pnBytes.size))
+	}
+	return err
+}
+
+func (pd pnDecoder) unmarshalBytes(v *[]byte) error {
+	pnBytes, err := pd.unmarshalPnBytes("[]byte")
+	*v = C.GoBytes(unsafe.Pointer(pnBytes.start), C.int(pnBytes.size))
+	return err
+}
+
+func (pd pnDecoder) unmarshalSymbol(v *Symbol) error {
+	pnBytes, err := pd.unmarshalPnBytes("symbol")
+	if err == nil {
+		*v = Symbol(C.GoStringN(pnBytes.start, C.int(pnBytes.size)))
+	}
+	return err
+}
+
+/*
+Unmarshal decodes AMQP-encoded bytes and stores the result in the value pointed to by v.
+
+FIXME mapping details
+
+ +-------------------------------+-----------------------------------------------+
+ |AMQP type                      |Go type                                        |
+ +-------------------------------+-----------------------------------------------+
+ |string                         |string                                         |
+ +-------------------------------+-----------------------------------------------+
+ |symbol                         |proton.Symbol                                  |
+ +-------------------------------+-----------------------------------------------+
+ |binary                         |[]byte                                         |
+ +-------------------------------+-----------------------------------------------+
+*/
+func Unmarshal(bytes []byte, v interface{}) error {
+	pd := newPnDecoder()
+	defer pd.free()
+	_, err := pd.unmarshal(bytes, v)
+	return err
+}
+
+// Decoder decodes AMQP values from an io.Reader.
+//
+type Decoder struct {
+	reader  io.Reader
+	buffer  bytes.Buffer
+	readErr error // Outstanding error on our reader
+}
+
+// NewDecoder returns a new decoder that reads from r.
+//
+// The decoder has it's own buffer and may read more data than required for the
+// AMQP values requested.  Use Buffered to see if there is data left in the
+// buffer.
+//
+func NewDecoder(r io.Reader) *Decoder {
+	return &Decoder{r, bytes.Buffer{}, nil}
+}
+
+// Buffered returns a reader of the data remaining in the Decoder's buffer. The
+// reader is valid until the next call to Decode.
+//
+func (d *Decoder) Buffered() io.Reader {
+	return bytes.NewReader(d.buffer.Bytes())
+}
+
+// more reads more data when we can't parse a complete AMQP type
+func (d *Decoder) more() error {
+	if d.readErr != nil { // Reader already broken, give up
+		return d.readErr
+	}
+	var readSize int64 = 256
+	if int64(d.buffer.Len()) > readSize { // Grow by doubling
+		readSize = int64(d.buffer.Len())
+	}
+	var n int64
+	n, d.readErr = d.buffer.ReadFrom(&io.LimitedReader{d.reader, readSize})
+	if n == 0 { // ReadFrom won't report io.EOF, just returns 0
+		if d.readErr != nil {
+			return d.readErr
+		} else {
+			return errorf("no data")
+		}
+	}
+	return nil
+}
+
+// Decode reads the next AMQP value from the Reader and stores it in the value pointed to by v.
+//
+// See the documentation for Unmarshal for details about the conversion of AMQP into a Go value.
+//
+func (d *Decoder) Decode(v interface{}) (err error) {
+	pd := newPnDecoder()
+	defer pd.free()
+
+	// On errEOS, read more data and try again till we have a complete pn_data.
+	for {
+		var n int
+		n, err = pd.unmarshal(d.buffer.Bytes(), v)
+		switch err {
+		case nil:
+			d.buffer.Next(n)
+			return
+		case errEOS:
+			err = d.more()
+			if err != nil {
+				return err
+			}
+		default:
+			return err
+		}
+	}
+	return err
+}

http://git-wip-us.apache.org/repos/asf/qpid-proton/blob/84bea240/proton-c/bindings/go/src/apache.org/proton/error.go
----------------------------------------------------------------------
diff --git a/proton-c/bindings/go/src/apache.org/proton/error.go b/proton-c/bindings/go/src/apache.org/proton/error.go
index 178303e..51cde28 100644
--- a/proton-c/bindings/go/src/apache.org/proton/error.go
+++ b/proton-c/bindings/go/src/apache.org/proton/error.go
@@ -71,12 +71,21 @@ func (code errorCode) Error() string {
 	return fmt.Sprintf("proton: %v", code)
 }
 
-// errorf formats an error message with a proton: prefix.
-func errorf(format string, a ...interface{}) error {
-	return fmt.Errorf("proton: %v", fmt.Sprintf(format, a...))
-}
+// pnError is a simple error string.
+//
+// NOTE: All error types used in proton have both String() and Error() methods.
+// The String() method prints the plain error message, the Error() method
+// prints the error message with a "proton:" prefix.
+// Thus you can format nested error messages with "%s" without getting nested "proton:"
+// prefixes but the prefix will be added when the end user uses Error()
+// or "%v" on the error value.
+//
+type pnError string
 
-// errorf2 formats an error message with a proton: prefix and an inner error message.
-func errorf2(err error, format string, a ...interface{}) error {
-	return fmt.Errorf("proton: %v: %v", fmt.Sprintf(format, a...), err)
+func (err pnError) String() string { return string(err) }
+func (err pnError) Error() string  { return fmt.Sprintf("proton: %s", string(err)) }
+
+// errorf creates an error with a formatted message
+func errorf(format string, a ...interface{}) error {
+	return pnError(fmt.Sprintf(format, a...))
 }

http://git-wip-us.apache.org/repos/asf/qpid-proton/blob/84bea240/proton-c/bindings/go/src/apache.org/proton/interop_test.go
----------------------------------------------------------------------
diff --git a/proton-c/bindings/go/src/apache.org/proton/interop_test.go b/proton-c/bindings/go/src/apache.org/proton/interop_test.go
new file mode 100644
index 0000000..db988d2
--- /dev/null
+++ b/proton-c/bindings/go/src/apache.org/proton/interop_test.go
@@ -0,0 +1,121 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+*/
+
+// Test that conversion of Go type to/from AMQP is compatible with other
+// bindings.
+//
+// FIXME aconway 2015-03-01: this should move to proton/tests/go when we integrate
+// better with the proton build system.
+//
+package proton
+
+import (
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os"
+	"reflect"
+	"testing"
+)
+
+func getReader(t *testing.T, name string) (r io.Reader) {
+	r, err := os.Open("../../../../../../tests/interop/" + name + ".amqp")
+	if err != nil {
+		t.Fatalf("Can't open %#v: %v", name, err)
+	}
+	return
+}
+
+func remaining(d *Decoder) string {
+	remainder, _ := ioutil.ReadAll(io.MultiReader(d.Buffered(), d.reader))
+	return string(remainder)
+}
+
+// Expectation of a test, want is the expected value, got is a pointer to a
+// instance of the same type, which will be replaced by Decode.
+type expect struct {
+	want, got interface{}
+}
+
+// checkDecode: want is the expected value, gotPtr is a pointer to a
+// instance of the same type for Decode.
+func checkDecode(d *Decoder, want interface{}, gotPtr interface{}) error {
+	err := d.Decode(gotPtr)
+	if err != nil {
+		return err
+	}
+	got := reflect.ValueOf(gotPtr).Elem().Interface()
+	if !reflect.DeepEqual(want, got) {
+		return fmt.Errorf("%#v != %#v", want, got)
+	}
+	return nil
+}
+
+func TestUnmarshal(t *testing.T) {
+	bytes, err := ioutil.ReadAll(getReader(t, "strings"))
+	if err != nil {
+		t.Error(err)
+	}
+	var got string
+	err = Unmarshal(bytes, &got)
+	if err != nil {
+		t.Error(err)
+	}
+	want := "abc\000defg"
+	if want != got {
+		t.Errorf("%#v != %#v", want, got)
+	}
+}
+
+func TestStrings(t *testing.T) {
+	d := NewDecoder(getReader(t, "strings"))
+	// Test decoding as plain Go strings
+	for i, want := range []string{"abc\000defg", "abcdefg", "abcdefg", "", "", ""} {
+		var got string
+		if err := checkDecode(d, want, &got); err != nil {
+			t.Errorf("%d: %v", i, err)
+		}
+	}
+	remains := remaining(d)
+	if remains != "" {
+		t.Errorf("leftover: %s", remains)
+	}
+
+	// Test decoding as specific string types
+	d = NewDecoder(getReader(t, "strings"))
+	var bytes []byte
+	var str string
+	var sym Symbol
+	for i, expect := range []expect{
+		{[]byte("abc\000defg"), &bytes},
+		{"abcdefg", &str},
+		{Symbol("abcdefg"), &sym},
+		{make([]byte, 0), &bytes},
+		{"", &str},
+		{Symbol(""), &sym},
+	} {
+		if err := checkDecode(d, expect.want, expect.got); err != nil {
+			t.Errorf("%d: %v", i, err)
+		}
+	}
+	remains = remaining(d)
+	if remains != "" {
+		t.Errorf("leftover: %s", remains)
+	}
+}

http://git-wip-us.apache.org/repos/asf/qpid-proton/blob/84bea240/proton-c/bindings/go/src/apache.org/proton/types.go
----------------------------------------------------------------------
diff --git a/proton-c/bindings/go/src/apache.org/proton/types.go b/proton-c/bindings/go/src/apache.org/proton/types.go
new file mode 100644
index 0000000..bde1ddd
--- /dev/null
+++ b/proton-c/bindings/go/src/apache.org/proton/types.go
@@ -0,0 +1,25 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+*/
+
+package proton
+
+// Types to exactly represent specific AMQP encodings
+
+// Symbol is the AMQP symbol data type, it can be converted to a Go string or []byte
+type Symbol string

http://git-wip-us.apache.org/repos/asf/qpid-proton/blob/84bea240/proton-c/bindings/go/src/apache.org/proton/url.go
----------------------------------------------------------------------
diff --git a/proton-c/bindings/go/src/apache.org/proton/url.go b/proton-c/bindings/go/src/apache.org/proton/url.go
index 44ef4ba..1511203 100644
--- a/proton-c/bindings/go/src/apache.org/proton/url.go
+++ b/proton-c/bindings/go/src/apache.org/proton/url.go
@@ -66,7 +66,7 @@ func ParseURL(s string) (u *url.URL, err error) {
 	path := C.GoString(C.pn_url_get_path(pnUrl))
 
 	if err != nil {
-		return nil, errorf2(err, "bad URL %#v", s)
+		return nil, errorf("bad URL %#v: %s", s, err)
 	}
 	if scheme == "" {
 		scheme = AMQP


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@qpid.apache.org
For additional commands, e-mail: commits-help@qpid.apache.org