Go, REST APIs, and Pointers
willnorris.com
willnorris.com
He ends up using pointers and mentions: "Using pointers also means that clients of the library will need to perform their own nil checks where appropriate to prevent panics."
Looking forward to using Rust :)
In practice, as your Rust comment alludes, I find working with nil properties in Go types (used to shuffle between the DB and JSON) to be quite painful. I end up keeping track of a lot more state than I really want to, and errors can be introduced subtly.
In Go, you can usually use a type's zero value without doing anything special. But if that type contains any nil properties, you can't presume this anymore. If I have:
type Thing struct {
ID int
Bad *string
}
I then have to do nil checks in every method that might handle Thing, or I have to make it accessible only via functions which perform those checks for me in advance. Either way, much less convenient.We opted for the JSON PATCH notation in RFC 6902: http://tools.ietf.org/html/rfc6902
Essentially there's a standard format for instructions on how to modify an existing JSON document, like this:
PATCH /my/data HTTP/1.1
Host: example.org
Content-Length: 326
Content-Type: application/json-patch+json
If-Match: "abc123"
[
{ "op": "test", "path": "/a/b/c", "value": "foo" },
{ "op": "remove", "path": "/a/b/c" },
{ "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] },
{ "op": "replace", "path": "/a/b/c", "value": 42 },
{ "op": "move", "from": "/a/b/c", "path": "/a/b/d" },
{ "op": "copy", "from": "/a/b/d", "path": "/a/b/e" }
]
We've implemented the operations "add", "remove" and "replace" in our REST API.We don't have a meaningful way of doing "move" and "copy", and "test" can be done by performing a GET and looking at the document (and that can be used in the If-Match).
You can see the documentation for our stuff here: http://microcosm-cc.github.io/#events-single-patch
And that gives away why we chose to do this... booleans and the default value of false. We wanted it to be more explicit and no room for accidental expression of a value anywhere, regardless of the callee language/environment or ours. JSON PATCH makes this very explicit.
Of course there's the mild inconvenience of handling the value type, but that's relatively easily overcome.
What was really interesting was handling permissions for the PATCH instructions.
For example a user might have permission to issue a PATCH that changed a string, but only the admin could issue a PATCH that updated some special part of a resource.
PATCH is relatively easy with the above, and very predictable... fine-grained permissions of which part of a document someone can update... that's definitely where the fun is.
foo := sql.NullString{"bar",true}
if foo.Valid {
fmt.Println(foo.String)
} func (ns NullString) MarshalJSON() ([]byte, error) {
if !ns.Valid {
return []byte("null"), nil
}
return json.Marshal(ns.String)
}
It would render `user_id: null` instead of `user_id: ""` which should work, I think. type Repository struct {
Name string `json:"name"`
Description string `json:"description"`
Private bool `json:"private"`
}
// response to blog post https://willnorris.com/2014/05/go-rest-apis-and-pointers
type RepositoryPatch struct {
Repository
patched_fields []string
}
func (rp *RepositoryPatch) SetPrivate(private bool) {
rp.patch_fields = append(rp.patch_fields,"private")
rp.Private = private
}
func (rp *RepositoryPatch) MarshalJSON() ([]byte, error) {
// marshal rp to []byte then unmarshal to map[string]interface{}
// filter patched_fields and then marshal again
// ;)
}Check out this solution we came up with in a testing/learning project: https://github.com/cranberryofdoom/peeps/blob/master/app/con.... Basically we just had to do an extra check to see if the fields actually existed in the form data. Will probably generalize it later on to make it less verbose.
Fetch struct A from DB
Unmarshal JSON data into &A
Unset read-only fields of A
Save A to DB
It's very inefficient but it beats dealing with bugs caused by various flags not being set (or being unset).I'll switch to using pointers to boolean, int and string since most other (non-trivial) fields are pointers anyway. I'm not happy about the indirection, but it's certainly much faster than the current database roundtrip I'm performing.
And for anyone shouting 'Rust' - there's always going to be that perfect language out there, at the end of the rainbow.
(That would be awesome!)
func ( rep Repository ) Update( map[string]interface{} )In go, shrug, hell if I know what anyone's doing with that language.