From c229d413e8f3dcca8e9511335802210c842ec27c Mon Sep 17 00:00:00 2001 From: Adam Lehechka <42357034+alehechka@users.noreply.github.com> Date: Sun, 15 May 2022 10:23:06 -0500 Subject: [PATCH] Create Documentation (#13) * start official docs * finish top-level resource * remove wip docs * extending node method section * finish links and errors * revisions --- README.md | 359 +++++++++++++++++++++++++++++++++++++++++++ documentation-wip.md | 117 -------------- 2 files changed, 359 insertions(+), 117 deletions(-) delete mode 100644 documentation-wip.md diff --git a/README.md b/README.md index 96680bb..08db926 100644 --- a/README.md +++ b/README.md @@ -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) + + + +[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) diff --git a/documentation-wip.md b/documentation-wip.md deleted file mode 100644 index 5450fa6..0000000 --- a/documentation-wip.md +++ /dev/null @@ -1,117 +0,0 @@ -# go-jsonapi - -This go module provides a useful SDK to create [JSON:API][jsonapi] HTTP interfaces. The primary usage of this library is to facilitate transformation from a flattened Go struct into the standardized JSON:API [resource object][jsonapi-resource-object]. - -With this, there are also struct interface options to facilitate relationships, metadata, and links, further information [below]. - -To help with standard error messaging, there also some available middleware functions available for use out-of-the-box if using [gin][gin]. - -## Installation - -Currently, this module is built with Go 1.18, but has yet to implement anything with Generics so it should be fairly backwards compatible. - -Install with: - -```bash -go get github.com/alehechka/go-jsonapi -``` - -> gin will be listed as an indirect dependency, but the core of the library only uses core Go libraries. - -## Usage - -This library does not create any unnecessary marshalling assumptions, it will simply take in `Response` or `CollectionResponse` objects and transform them into JSON:API structs that keep the same provided object as the `Attributes` value. This allows an overlaying marshalling implementation to take over after transformation. - -View examples here: [/examples](/examples) - -### Interface Methods - -This library is built with struct interfaces at its core, so any object that you'd like to transform into JSON:API must implement both the `ID() string` and `Type() string` methods. However, there are more optional methods that can be implemented to further extend the JSON:API transformed object. - -
ID() string - -The `ID()` interface is always required and is used to select a data member from the struct to use as the `id` field in the JSON:API object. - -> It is recommended to omit the selected variable with the following tag: -> -> ```go -> RecordID string `json:"-"` -> ``` -> -> Although this is not required. - -```go -func (record Record) ID() string { - return record.RecordID -} -``` - -
- -
Links() jsonapi.Links - -```go -func (record Record) Links() jsonapi.Links { - return jsonapi.Links{ - jsonapi.SelfKey: { - Href: "/records/:id", - Params: jsonapi.Params{ - "id": record.ID(), - }, - Queries: jsonapi.Queries{ - "page[size]": 10, - }, - // Meta: jsonapi.Meta{ - // "page": 10, - // }, - } - } -} -``` - -
-Resulting JSON -A `Record` object with `RecordID=1234` would have a resulting `links` object as follows: - -```json -{ - "links": { - "self": "http://example.com/records/1234?page[size]=10" - } -} -``` - -If the commented `Meta` option were used the resulting `self` link would be an object as follows: - -```json -{ - "links": { - "self": { - "href": "http://example.com/records/1234?page[size]=10", - "meta": { - "page": 10 - } - } - } -} -``` - -
- -The `Href` variable is recommended to be provided a relative path and will be populated with a hostname from the provided `*http.Request` object. When no `Meta` variable is provided, the resulting `Link` will be a string of the generated URL. - -The `Href` variable can also be created with path params in the prefixed with a colon `:` and will be substituted in with a provided matching key in the `Params` variable. - -The `Params` variable represents a `map[string]any` and all matching keys will be substituted into the `Href`. `Params` will always be omitted when marshalling json. - -The `Queries` variable represents a `map[string]any` and will generate query parameters to append to the `Href`. `Queries` will always be omitted when marshalling json. - -The `Meta` variable represents a `map[string]any` and when provided will generate that `Link` as an object with the `meta` object included. - -
- - - -[jsonapi]: (https://jsonapi.org/) -[jsonapi-resource-object]: (https://jsonapi.org/format/#document-resource-objects) -[gin]: (https://github.com/gin-gonic/gin)