Skip to content

Commit

Permalink
Create Documentation (#13)
Browse files Browse the repository at this point in the history
* start official docs

* finish top-level resource

* remove wip docs

* extending node method section

* finish links and errors

* revisions
  • Loading branch information
alehechka authored May 15, 2022
1 parent 4c7c318 commit c229d41
Show file tree
Hide file tree
Showing 2 changed files with 359 additions and 117 deletions.
359 changes: 359 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,360 @@
# go-jsonapi

This Go module provides a useful API to create [JSON:API][jsonapi] HTTP servers. The primary usage of this library is to facilitate transformation from flattened Go structs into the standardized JSON:API [resource object][jsonapi-resource-object].

Additionally, there are optional methods that can be implemented with structs to add further standardized JSON:API structures such as links, relationships, included data, and metadata.

## Installation

```bash
go get github.com/alehechka/go-jsonapi
```

Import as:

```go
import "github.com/alehechka/go-jsonapi/jsonapi"
```

## Usage

### Defining a JSON:API struct

The primary resource object in JSON:API is of the following type:

```json
{
"data": {
"id": "1234",
"type": "people",
"attributes": {
"firstName": "John",
"lastName": "Doe",
"age": 30
}
}
}
```

- The `attributes` object will be generated from the struct itself.
- The `id` field will be populated by the `ID()` interface method.
- The `type` field will be populated by the `Type()` interface method.

```go
type Person struct {
// It is recommended to omit the primary ID from json marshalling, but not required
PersonID string `json:"-"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Age int `json:"age"`
}

func (person Person) ID() string {
return person.PersonID
}

func (person Person) Type() string {
return "people"
}
```

### Prepare for JSON marshalling

To prepare the struct for json marshalling it is required to use the provided `TransformResponse` or `TransformCollectionResponse` functions:

```go
response := jsonapi.TransformResponse(jsonapi.Response{
Node: Person{},
"http://example.com",
})

response := jsonapi.TransformCollectionResponse(jsonapi.CollectionResponse{
Nodes: []Person{},
"http://example.com",
})
```

The second parameter to these functions is for `baseURL`, this is used to dynamically populate relative URLs in `links` objects. More on this [here](#links).

### Recommended Usage

The above functions are effectively the top-level transformation tools, however, the dynamic link creation can be made easy by supplying an `*http.Request` object to the following functions instead:

```go
req := httptest.NewRequest("GET", "http://example.com/example", nil)

response := jsonapi.CreateResponse(req)(jsonapi.Response{
Node: Person{},
})

response := jsonapi.CreateCollectionResponse(req)(jsonapi.CollectionResponse{
Nodes: []Person{},
})
```

These versions will automatically extract the baseURL from the request and supply it to the respective `Transform` functions outlined above. This allows all generated links to display the same scheme and hostname as the server domain that the request was originally made to.

Additionally, using the `Create` functions will automatically generate a `self` link at the top-level object for every response.

### Extending the top-level resource

The JSON:API spec also allows for `links`, `errors`, and `meta` objects at the top-level of the document. Both `jsonapi.Response` and `jsonapi.CollectionResponse` have values available for these.

#### Links

A top-level `links` object can be provided to both `Response` and `CollectionResponse`. See [Link](#link) below for further details.

```go
res := jsonapi.Response{
Links: jsonapi.Links{
jsonapi.NextKey: jsonapi.Link{
Href: "/path/to/next/resource",
},
},
}
```

> When using either `CreateResponse` or `CreateCollectionResponse` the `self` link will be automatically generated and always override an existing `self` link.
#### Meta

A top-level `meta` object can be provided to both `Response` and `CollectionResponse` in the form of any interface or key-value map.

```go
res := jsonapi.Response{
Meta: jsonapi.Meta{
"page": jsonapi.Meta{
"size": 10,
"number": 2,
},
},
}
```

> The `Meta` struct is simply an alias for `map[string]interface{}`
#### Errors

A top-level `errors` array can be provided to both `Response` and `CollectionResponse` in the form of an array of `Error` objects. See [Error](#error) below for further detail.

```go
res := jsonapi.Response{
Errors: jsonapi.Errors{
{
Status: http.StatusBadRequest,
Title: "Error Occurred",
Detail: "Failed to retrieve resource",
},
},
}
```

> It is important to note that if at least 1 error is present in this array than the top-level `data` object/array and `included` array will not be available as per the JSON:API spec for [Top Level][jsonapi-top-level].
### Extending `Node` interface

By default, to be considered a JSON:API resource, a struct must include the `ID()` and `Type()` methods.

However, this functionality can be extended further with other methods as follows:

#### `Links()`

The `Links()` method allows an individual resource to generate the `links` object for itself using data from the object. See [Link](#link) below for further details.

```go
func (person Person) Links() jsonapi.Links {
return jsonapi.Links{
jsonapi.SelfKey: jsonapi.Link{
Href: "/people/:id",
Params: jsonapi.Params{
"id": person.ID(),
}
},
}
}
```

The above scenario makes use of the `Params` field which will not be included in the resulting json, but will use the key-value pairs to substitute the values into the `href` based on keys that it finds. (Ex. `:id` in the href will be substituted with the value of `person.ID()`)

#### `Relationships()`

[Relationships][jsonapi-relationships] are a key object within a resource to provide linkage and information about related resources. To facilitate the mapping, the `Relationships()` method gives access to the parent struct and allows definition of the `relationships` map as follows:

```go
type Company struct {
CompanyID string `json:"-"`
Name string `json:"name"`
Address string `json:"address"`
Employees []Person `json:"-"` // recommended to omit children resources
Owner Person `json:"-"`
}

func (company Company) Relationships() map[string]interface{
return map[string]interface{}{
"employees": company.Employees,
"owner": company.Owner,
}
}
```

> In the above example it is crucial that the children relationship objects adhere to the JSON:API methods, i.e. initialize their own `ID()` and `Type()` methods.
#### `RelationshipLinks(parentID string)`

Typically in the `relationships` object, there will be included `links` object with links to the [related resources][jsonapi-related-links]. This can be facilitated by included the `RelationshipLinks(parentID string`) on children structs. The `parentID` parameter will automatically be supplied when generated as part of a relationship by the parent struct, it is recommended to use this in generating path params for the href variable.

```go
func (person Person) RelationshipLinks(companyID string) jsonapi.Links {
return jsonapi.Links{
jsonapi.SelfKey: jsonapi.Link{
Href: "/companies/:companyID/relationships/employees",
Params: jsonapi.Params{
"companyID": companyID,
},
},
jsonapi.RelatedKey: jsonapi.Link{
Href: "/companies/:companyID/employees",
Params: jsonapi.Params{
"companyID": companyID,
},
},
}
}
```

If the relationship will point to an array of resources, it is recommended to instead create a unique type for that array of structs as follows:

```go
type People []Person

func (people People) RelationshipLinks(companyID string) jsonapi.Links {
return jsonapi.Links{
jsonapi.SelfKey: jsonapi.Link{
Href: "/companies/:companyID/relationships/employees",
Params: jsonapi.Params{
"companyID": companyID,
},
},
}
}
```

#### `Meta()`

The `Meta()` method is simply a means to generate a `meta` object for an individual resource by using the object as an input.

```go
func (person Person) Meta() interface{} {
return jsonapi.Meta{
"fullName": fmt.Sprintf("%s %s", person.FirstName, person.LastName),
}
}
```

### Structs Explained

#### `Link`

The JSON:API [Links][jsonapi-document-links] states that each value of the `links` map must either be a string containing the link's URL or an object with an `href` and `meta` object. By, default, a `Link` object will be transformed into the string format in all cases expect when a non-nil, non-empty `Meta` object is provided.

```go
links := jsonapi.Links{
"self": jsonapi.Link{
Href: "/path/to/resource",
},
"next": jsonapi.Link{
Href: "/path/to/next/resource",
Meta: jsonapi.Meta{
"page": 3,
},
},
}
```

After transformation and JSON marshalling assuming the provided `baseURL` is `http://example.com`, the result will be as follows:

```json
{
"links": {
"self": "http://example.com/path/to/resource",
"next": {
"href": "http://example.com/path/to/next/resource",
"meta": {
"page": 3
}
}
}
}
```

Additionally, the `Link` object provides options for `Params` and `Queries`. These will always be ignored in the JSON marshalling and are used to help generate the `href` URL.

- `Params` is a map of key-value pairs that represent path parameters. During transformation, href path sections that are prefixed with a colon (`:`), will be substituted with the value of a matching key in the `Params` map.
- `Queries` is a map of key-value pairs that represent query parameters. During transformation, all key-value pairs will be generated and appended to the href as query parameters. Pre-existing query parameters in the supplied href will not be removed, but will be replaced if they have the same key.

```go
links := jsonapi.Links{
"self": jsonapi.Link{
Href: "/path/to/resource/:id?page[size]=20"
Params: jsonapi.Params{
"id": 1234,
},
Queries: jsonapi.Queries{
"page[number]": 4,
},
},
}
```

After transformation and JSON marshalling assuming the provided `baseURL` is `http://example.com`, the result will be as follows:

```json
{
"links": {
"self": "http://example.com/path/to/resource/1234?page[size]=20&page[limit]=4"
}
}
```

> For further details, view the implementation here: [/jsonapi/links.go](/jsonapi/links.go#L35-L44)
#### `Error`

The JSON:API `Errors` specification includes a large number of fields, all of which can be supplied to the provided `Error` object. The internal `links` object of `Error` will also be supplied the `baseURL` and follow the same transformation rules outlined [above](#link).

```go
errs := jsonapi.Errors{
{
Status: http.StatusBadRequest,
Title: "Standard Error Occurred",
Detail: "Further Detail is supplied here",
},
}
```

After transformation and JSON marshalling, the result will be as follows:

```json
{
"errors": [
{
"status": 400,
"title": "Standard Error Occurred",
"detail": "Further Detail is supplied here"
}
]
}
```

> For further details, view the implementation here: [/jsonapi/errors.go](/jsonapi/errors.go#L10-L19)
<!--- Links -->

[jsonapi]: (https://jsonapi.org/)
[jsonapi-resource-object]: (https://jsonapi.org/format/#document-resource-objects)
[jsonapi-top-level]: (https://jsonapi.org/format/#document-top-level)
[jsonapi-relationships]: (https://jsonapi.org/format/#document-resource-object-relationships)
[jsonapi-related-links]: (https://jsonapi.org/format/#document-resource-object-related-resource-links)
[jsonapi-document-links]: (https://jsonapi.org/format/#document-links)
[jsonapi-errors]: (https://jsonapi.org/format/#errors)
[gin]: (https://github.com/gin-gonic/gin)
Loading

0 comments on commit c229d41

Please sign in to comment.