Skip to content

Commit

Permalink
Merge pull request #6 from derekdowling/wip-better-data-parsing
Browse files Browse the repository at this point in the history
Wip better data parsing
  • Loading branch information
Derek Dowling committed Dec 22, 2015
2 parents 46894e8 + a3d742d commit 0717a1d
Show file tree
Hide file tree
Showing 23 changed files with 771 additions and 558 deletions.
37 changes: 24 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
Go JSON API Specification Handler
---

[![GoDoc](https://godoc.org/github.com/derekdowling/go-json-spec-handler?status.png)](https://godoc.org/github.com/derekdowling/go-json-spec-handler)
[![Build Status](https://travis-ci.org/derekdowling/go-json-spec-handler.svg?branch=master)](https://travis-ci.org/derekdowling/go-json-spec-handler)
[![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](https://godoc.org/github.com/derekdowling/go-json-spec-handler)
[![Travis CI](https://img.shields.io/travis/derekdowling/go-json-spec-handler/master.svg?style=flat-square)](https://travis-ci.org/derekdowling/go-json-spec-handler)
[![Go Report Card](http://goreportcard.com/badge/manyminds/api2go)](http://goreportcard.com/report/derekdowling/go-json-spec-handler)
[TestCoverage](http://gocover.io/github.com/derekdowling/go-json-spec-handler?version=1.5rc1)

An HTTP Client and Server request/response handler for dealing with [JSON Specification](http://jsonapi.org/)
APIs. Great for Ember.js!
A server (de)serialization handler for creating [JSON API Specification](http://jsonapi.org/)
compatible backends in Go. Works with [Ember-Data](https://github.com/emberjs/data) too!

# Packages

Expand Down Expand Up @@ -55,7 +55,6 @@ func PatchUser(w http.ResponseWriter, r *http.Request) {
}
```


### jsc - JSON Specification Client

HTTP JSON Client for interacting with JSON APIs. Built on top of http.Client
Expand All @@ -69,6 +68,21 @@ object, response, err := jsc.GetObject("http://your.api/", "user", "1")
```


### Philosophy Behind JSH

In sticking with Go's philosophy of modules over frameworks, `jsh` was created
to be a drop in serialization layer focusing only on parsing, validating, and
sending JSON API compatible responses. Currently `jsh` is getting fairly close
to stable. It's undergone a number of large refactors to accomodate new
aspects of the specification as I round out the expected feature set which is
pretty well completed, including support for the HTTP client linked above.

If you're looking for a good place to start with a new API, I've since created
[jshapi](https://github.com/derekdowling/jsh-api) which builds on top of [Goji 2](https://goji.io/)
and `jsh` in order to handle the routing structure that JSON API requires as
well as a number of other useful tools for testing and mocking APIs as you
develop your own projects.

### Features

Implemented:
Expand All @@ -83,25 +97,22 @@ object, response, err := jsc.GetObject("http://your.api/", "user", "1")

TODO:

- Reserved character checking
- [Reserved character checking](http://jsonapi.org/format/upcoming/#document-member-names-reserved-characters)

Not Implenting:
Not Implementing:

* These features aren't handled because they are beyond the scope of what
this library is meant to be. In the future, I might build a framework
utilizing this library to handle these complex features which require
Router and ORM compatibility.
this module is meant to be. See [jshapi](https://github.com/derekdowling/jsh-api)
if these are problems that you'd also like to have solved.

- Routing
- Relationship management
- Sorting
- Pagination
- Filtering

## Examples

- [jshapi](https://github.com/derekdowling/jsh-api) abstracts the full
serialization layer for JSON Spec APIs.

There are lots of great examples in the tests themselves that show exactly how jsh works.
The [godocs](https://godoc.org/github.com/derekdowling/go-json-spec-handler) as linked above
have a number of examples in them as well.
36 changes: 19 additions & 17 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,16 @@ import (
"github.com/derekdowling/go-json-spec-handler"
)

// ParseObject validates the HTTP response and parses out the JSON object from the
// body if possible
func ParseObject(response *http.Response) (*jsh.Object, *jsh.Error) {
return buildParser(response).GetObject()
}
// JSON validates the HTTP response and attempts to parse a JSON API compatible
// Document from the response body before closing it
func JSON(response *http.Response) (*jsh.JSON, *jsh.Error) {
document, err := buildParser(response).JSON(response.Body)
if err != nil {
return nil, err
}

// ParseList validates the HTTP response and parses out the JSON list from the
// body if possible
func ParseList(response *http.Response) (jsh.List, *jsh.Error) {
return buildParser(response).GetList()
document.HTTPStatus = response.StatusCode
return document, nil
}

// DumpBody is a convenience function that parses the body of the response into a
Expand All @@ -40,7 +40,6 @@ func buildParser(response *http.Response) *jsh.Parser {
return &jsh.Parser{
Method: "",
Headers: response.Header,
Payload: response.Body,
}
}

Expand Down Expand Up @@ -84,7 +83,7 @@ func objectToPayload(request *http.Request, object *jsh.Object) ([]byte, *jsh.Er

// sendPayloadRequest is required for sending JSON payload related requests
// because by default the http package does not set Content-Length headers
func sendObjectRequest(request *http.Request, object *jsh.Object) (*jsh.Object, *http.Response, *jsh.Error) {
func doObjectRequest(request *http.Request, object *jsh.Object) (*jsh.JSON, *http.Response, *jsh.Error) {

payload, err := objectToPayload(request, object)
if err != nil {
Expand All @@ -94,21 +93,24 @@ func sendObjectRequest(request *http.Request, object *jsh.Object) (*jsh.Object,
// prepare payload and corresponding headers
request.Body = jsh.CreateReadCloser(payload)
request.Header.Add("Content-Type", jsh.ContentType)
request.Header.Set("Content-Length", strconv.Itoa(len(payload)))

contentLength := strconv.Itoa(len(payload))
request.ContentLength = int64(len(payload))
request.Header.Set("Content-Length", contentLength)

client := &http.Client{}
response, clientErr := client.Do(request)
httpResponse, clientErr := client.Do(request)

if clientErr != nil {
return nil, nil, jsh.ISE(fmt.Sprintf(
"Error sending %s request: %s", request.Method, clientErr.Error(),
))
}

object, objErr := ParseObject(response)
if objErr != nil {
return nil, response, objErr
document, err := JSON(httpResponse)
if err != nil {
return nil, httpResponse, err
}

return object, response, nil
return document, httpResponse, nil
}
15 changes: 10 additions & 5 deletions client/client_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package jsc

import (
"log"
"net/url"
"testing"

Expand Down Expand Up @@ -51,12 +52,15 @@ func TestResponseParsing(t *testing.T) {
obj, objErr := jsh.NewObject("123", "test", map[string]string{"test": "test"})
So(objErr, ShouldBeNil)
response, err := mockObjectResponse(obj)
log.Printf("response = %+v\n", response)
So(err, ShouldBeNil)

Convey("should parse successfully", func() {
respObj, err := ParseObject(response)
doc, err := JSON(response)

So(err, ShouldBeNil)
So(respObj.ID, ShouldEqual, "123")
So(doc.HasData(), ShouldBeTrue)
So(doc.First().ID, ShouldEqual, "123")
})
})

Expand All @@ -71,10 +75,11 @@ func TestResponseParsing(t *testing.T) {
So(err, ShouldBeNil)

Convey("should parse successfully", func() {
list, err := ParseList(response)
json, err := JSON(response)

So(err, ShouldBeNil)
So(list, ShouldNotBeNil)
So(list[0].ID, ShouldEqual, "123")
So(json.HasData(), ShouldBeTrue)
So(json.Data.List[0].ID, ShouldEqual, "123")
})
})
})
Expand Down
46 changes: 16 additions & 30 deletions client/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import (
"github.com/derekdowling/go-json-spec-handler"
)

// GetObject allows a user to make an outbound GET /resourceTypes/:id
func GetObject(urlStr string, resourceType string, id string) (*jsh.Object, *http.Response, *jsh.Error) {
// Fetch performs an outbound GET /resourceTypes/:id request
func Fetch(urlStr string, resourceType string, id string) (*jsh.JSON, *http.Response, *jsh.Error) {
if id == "" {
return nil, nil, jsh.SpecificationError("ID cannot be empty for GetObject request type")
}
Expand All @@ -21,47 +21,33 @@ func GetObject(urlStr string, resourceType string, id string) (*jsh.Object, *htt

setIDPath(u, resourceType, id)

response, err := Get(u.String())
if err != nil {
return nil, nil, err
}

object, err := ParseObject(response)
if err != nil {
return nil, response, err
}

return object, response, nil
return Get(u.String())
}

// GetList prepares an outbound request for /resourceTypes expecting a list return value.
func GetList(urlStr string, resourceType string) (jsh.List, *http.Response, *jsh.Error) {
// List prepares an outbound GET /resourceTypes request
func List(urlStr string, resourceType string) (*jsh.JSON, *http.Response, *jsh.Error) {
u, urlErr := url.Parse(urlStr)
if urlErr != nil {
return nil, nil, jsh.ISE(fmt.Sprintf("Error parsing URL: %s", urlErr.Error()))
}

setPath(u, resourceType)

response, err := Get(u.String())
if err != nil {
return nil, nil, err
}
return Get(u.String())
}

list, err := ParseList(response)
if err != nil {
return nil, response, err
// Get performs a generic GET request for a given URL and attempts to parse the
// response into a JSON API Format
func Get(urlStr string) (*jsh.JSON, *http.Response, *jsh.Error) {
response, httpErr := http.Get(urlStr)
if httpErr != nil {
return nil, nil, jsh.ISE(fmt.Sprintf("Error performing GET request: %s", httpErr.Error()))
}

return list, response, nil
}

// Get performs a Get request for a given URL and returns a basic Response type
func Get(urlStr string) (*http.Response, *jsh.Error) {
response, err := http.Get(urlStr)
json, err := JSON(response)
if err != nil {
return nil, jsh.ISE(fmt.Sprintf("Error performing GET request: %s", err.Error()))
return nil, nil, err
}

return response, nil
return json, response, nil
}
20 changes: 15 additions & 5 deletions client/get_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,36 @@ func TestGet(t *testing.T) {
Convey("Get Tests", t, func() {

Convey("->Get()", func() {
resp, err := Get(baseURL + "/tests/1")
json, resp, err := Get(baseURL + "/tests/1")

So(err, ShouldBeNil)
So(resp.StatusCode, ShouldEqual, http.StatusOK)
So(json.HasErrors(), ShouldBeFalse)
So(json.HasData(), ShouldBeTrue)
})

Convey("->GetList()", func() {

Convey("should handle an object listing request", func() {
list, _, err := GetList(baseURL, "test")
json, resp, err := List(baseURL, "test")

So(err, ShouldBeNil)
So(len(list), ShouldEqual, 1)
So(resp.StatusCode, ShouldEqual, http.StatusOK)
So(json.HasErrors(), ShouldBeFalse)
So(json.HasData(), ShouldBeTrue)
})
})

Convey("->GetObject()", func() {

Convey("should handle a specific object request", func() {
obj, _, err := GetObject(baseURL, "test", "1")
json, resp, err := Fetch(baseURL, "test", "1")

So(err, ShouldBeNil)
So(obj.ID, ShouldEqual, "1")
So(resp.StatusCode, ShouldEqual, http.StatusOK)
So(json.HasErrors(), ShouldBeFalse)
So(json.HasData(), ShouldBeTrue)
So(json.First().ID, ShouldEqual, "1")
})
})
})
Expand Down
18 changes: 9 additions & 9 deletions client/patch.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,23 @@ import (
// Patch allows a consumer to perform a PATCH /resources/:id request
// Example:
//
// obj, _ := jsh.NewObject("123", "resource_name", payload)
// resp, _ := jsc.Patch("http://postap.com", obj)
// updatedObj, _ := resp.GetObject()
// obj, _ := jsh.NewObject("123", "user", payload)
// // does PATCH /http://postap.com/api/user/123
// json, resp, err := jsc.Patch("http://postap.com/api/", obj)
// updatedObj := json.First()
//
func Patch(urlStr string, object *jsh.Object) (*jsh.Object, *http.Response, *jsh.Error) {

u, err := url.Parse(urlStr)
func Patch(baseURL string, object *jsh.Object) (*jsh.JSON, *http.Response, error) {
u, err := url.Parse(baseURL)
if err != nil {
return nil, nil, jsh.ISE(fmt.Sprintf("Error parsing URL: %s", err.Error()))
return nil, nil, fmt.Errorf("Error parsing URL: %s", err.Error())
}

setIDPath(u, object.Type, object.ID)

request, err := http.NewRequest("PATCH", u.String(), nil)
if err != nil {
return nil, nil, jsh.ISE(fmt.Sprintf("Error creating PATCH request: %s", err.Error()))
return nil, nil, fmt.Errorf("Error creating PATCH request: %s", err.Error())
}

return sendObjectRequest(request, object)
return doObjectRequest(request, object)
}
19 changes: 18 additions & 1 deletion client/patch_test.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,32 @@
package jsc

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/derekdowling/go-json-spec-handler"
"github.com/derekdowling/jsh-api"
. "github.com/smartystreets/goconvey/convey"
)

func TestPatch(t *testing.T) {

Convey("Patch Tests", t, func() {
resource := jshapi.NewMockResource("test", 1, nil)
server := httptest.NewServer(resource)
baseURL := server.URL

})
Convey("->Patch()", func() {
object, err := jsh.NewObject("2", "test", nil)
So(err, ShouldBeNil)

json, resp, patchErr := Patch(baseURL, object)

So(patchErr, ShouldBeNil)
So(resp.StatusCode, ShouldEqual, http.StatusOK)
So(json.HasErrors(), ShouldBeFalse)
So(json.HasData(), ShouldBeTrue)
})
})
}
Loading

0 comments on commit 0717a1d

Please sign in to comment.