pmoule / go2hal

A HAL implementation in Go with JSON generator.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

go2hal

Build codecov GoDoc License: MIT

A Go implementation of Hypertext Application Language (HAL). It provides essential data structures and features a JSON generator to produce JSON output as proposed in JSON Hypertext Application Language. As an add-on it also supports the The HAL-FORMS Media Type based on the Working Draft from 2021-03-03.

Features

  • HAL API
    • Create root Resource Object.

    • Supports "_links" property.

      Define Link Relations and assign Link Object value(s).

    • Supports "_embedded" property.

      Define Link Relations and assign Resource Object value(s).

    • Supports "curies".

      Define CURIE Link Objects and assign to defined Link Relations.

  • JSON generator to produce HAL Document
  • Tools to simplify HAL document creation
  • HAL-FORMS
    • JSON generator to produce HAL-FORMS documents
    • Supports creating relations in HAL pointing to HAL-FORMS documents

Usage

Preliminary stuff

Download and install go2hal into your GOPATH.

go get github.com/pmoule/go2hal

Import the hal package to get started.

import "github.com/pmoule/go2hal/hal"

Create a Resource and JSON generation

First create a Resource Object as your HAL document's root element.

root := hal.NewResourceObject()

This is all you need, to create a valid HAL document.

Next, create an Encoder and call it's ToJSON function to generate valid JSON.

encoder := hal.NewEncoder()
bytes, error := encoder.ToJSON(root)

Generated JSON

{}

There's potential for more 😄

Add a Link Relation

So let's add a self Link Relation. Additionally we attach a single Link Object.

link := &hal.LinkObject{ Href: "/docwhoapi/doctors"}

self, _ := hal.NewLinkRelation("self") //skipped error handling
self.SetLink(link)

root.AddLink(self)

Since self is a IANA registered link relation name, go2hal provides a shortcut

self := hal.NewSelfLinkRelation()

Generated JSON

{
    "_links": {
        "self": {
            "href": "/docwhoapi/doctors"
        }
    }
}

Resource state

To add some resource state, you can add some properties. These properties must be valid JSON

(Currently, this is not checked)

data := root.Data()
data["doctorCount"] = 12

Generated JSON

{
    "_links": {
        "self": {
            "href": "/docwhoapi/doctors"
        }
    },
    "doctorCount": 12
}

It might be a bit cumbersome to manually map all properties from any of your DTOs. There is a more convenient way.

// a simple struct
type DoctorsInfo struct {
    Content     string  `json:"content"`
    DoctorCount int     `json:"doctorCount"` 
    From        string  `json:"from"`
    Until       string  `json:"until"`
}

info := DoctorsInfo{12, "All actors of the Doctor.", "1963", "today"}
root.AddData(info)

Generated JSON

{
    "_links": {
        "self": {
            "href": "/docwhoapi/doctors"
        }
    },
    "content": "All actors of the Doctor."
    "doctorCount": 12,
    "from": "1963"
    "until": "today"
}

Both ways of adding state can be combined. But already existing properties are replaced.

Embedding Resources

Now, let's embed some resources.

// a simple struct for actors of the doctor
type Actor struct {
    ID   int    `json:"-"`
    Name string `json:"name"`
}

actors := []Actor {
    Actor{1, "William Hartnell"},
    Actor{2, "Patrick Troughton"},
}

// convert the actors to resources
var embeddedActors []hal.Resource

for _, actor := range actors {
    href := fmt.Sprintf("/docwhoapi/doctors/%d", actor.ID)
    selfLink, _ := hal.NewLinkObject(href)

    self, _ := hal.NewLinkRelation("self")
    self.SetLink(selfLink)

    embeddedActor := hal.NewResourceObject()
    embeddedActor.AddLink(self)
    embeddedActor.AddData(actor)
    embeddedActors = append(embeddedActors, embeddedActor)
}

doctors, _ := hal.NewResourceRelation("doctors")
doctors.SetResources(embeddedActors)

root.AddResource(doctors)

Generated JSON

{
    "_links": {
        "self": {
            "href": "/docwhoapi/doctors"
        }
    },
    "_embedded": {
        "doctors": [
            {
                "_links": {
                    "self": {
                        "href": "/docwhoapi/doctors/1"
                    }
                },
                "name": "William Hartnell"
            },
            {
                "_links": {
                    "self": {
                        "href": "/docwhoapi/doctors/2"
                    }
                },
                "name": "Patrick Troughton"
            }
        ]
    },
    "content": "All actors of the Doctor."
    "doctorCount": 12,
    "from": "1963"
    "until": "today"
}

CURIEs

A Resource Object can have a set of CURIE links. Same for used Link Relations, that are capable of setting a CURIE link.

curieLink, _ := hal.NewCurieLink("doc", "http://example.com/docs/relations/{rel}")
curieLinks := []*hal.LinkObject {curieLink}
root.AddCurieLinks(curieLinks)

doctors.SetCurieLink(curieLink)

Generated JSON

{
    "_links": {
        "curies": [
            {
                "href": "http://example.com/docs/relations/{rel}",
                "templated": true,
                "name": "doc"
            }
        ],
        "self": {
            "href": "/docwhoapi/doctors"
        }
    },
    "_embedded": {
            "doc:doctors": [
                {
                    "_links": {
                        "self": {
                            "href": "/docwhoapi/doctors/1"
                        }
                    },
                    "name": "William Hartnell"
                },
                {
                    "_links": {
                        "self": {
                            "href": "/docwhoapi/doctors/2"
                        }
                    },
                    "name": "Patrick Troughton"
                }
            ]
        },
    "doctorCount": 12
}

Relations and the array vs single value discussion

I'm aware of existing discussions regarding Relations and the type of assigned values. I simply deal with this topic by leaving the decision to the developer whether to assign a single value or an array value.

go2hal provides a LinkRelation and a ResourceRelation. Both are capable of holding a single value or an array value by providing special setter functions.

Example

link := hal.NewLinkObject("myHref")
linkRelation := hal.NewLinkRelation("linkRel")

resource := hal.NewResourceObject()
resource.Data()["value"] = "myValue"
resourceRelation := hal.NewResourceRelation("resourceRel")

Assign single values

linkRelation.SetLink(link)
resourceRelation.SetResource(resource)

Generated JSON

{
    "_links": {
        "linkRel": {
            "href": "myHref"
        }
    },
    "_embedded": {
            "resourceRel": {
                "value": "myValue"
            }
        },
}

Assign array values

linkRelation.SetLinks([]*LinkObject{link})
resourceRelation.SetResources([]hal.Resource{resource})

Generated JSON

{
    "_links": {
        "linkRel": [
            {
                "href": "myHref"
            }
        ]
    },
    "_embedded": {
        "resourceRel": [
            {
                "value": "myValue"
            }
        ]
    },
}

go2hal does not evaluate the assigned values. The developer is fully responsible of this.

CURIEs are an exception to this. As stated in the specification CURIEs are always an array of Link Objects.

Tooling

To simplify creating HAL documents, go2hal provides a ResourceFactory. Initialise it with a set of CURIE links.

curieLink, _ := hal.NewCurieLink("doc", "http://example.com/docs/relations/{rel}")
curieLinks := []*hal.LinkObject {curieLink}

factory := NewResourceFactory(curieLinks)

Create a root resource with it's self link.

self := "/docwhoapi/doctors"
root := factory.CreateRootResource(self)

Generated JSON

{
    "_links": {
        "curies": [
            {
                "href": "http://example.com/docs/relations/{rel}",
                "templated": true,
                "name": "doc"
            }
        ],
        "self": {
            "href": "/docwhoapi/doctors"
        }
    }
}

Create a link and an embedded resource and it's ResourceRelation.

//assign a CURIE link by name
companions := factory.CreateLink("companions", "/docwhoapi/companions", "doc")
root.AddLink(companions)

doctor := Actor{1, "William Hartnell"}
embeddedSelf := fmt.Sprintf("/docwhoapi/doctors/%d", doctor.ID)
embeddedDoctor := factory.CreateEmbeddedResource(embeddedSelf)
embeddedDoctor.AddData(doctor)

//assign a CURIE link by name
doctorLink := factory.CreateResourceLink("hartnell", "doc")
doctorLink.SetResource(embeddedDoctor)

root.AddResource(doctorLink)

Generated JSON

{
    "_links": {
        "curies": [
            {
                "href": "http://example.com/docs/relations/{rel}",
                "templated": true,
                "name": "doc"
            }
        ],
        "doc:companions": {
            "href": "/docwhoapi/companions"
        },
        "self": {
            "href": "/docwhoapi/doctors"
        }
    },
    "_embedded": {
        "doc:hartnell": [
            {
                "_links": {
                    "self": {
                        "href": "/docwhoapi/doctors/1"
                    }
                },
                "name": "William Hartnell"
            }
        ]
    },
}

HAL-FORMS

Let's create a link relation pointing to a HAL-FORMS document for creating a new resource.

createLink, _ := halforms.NewHALFormsRelation{"create", "/docwhoapi/hal-forms/create-doctor"} //skipped error handling
root.AddLink(createLink)

Generated JSON

{
    "_links": {
        "create": {
            "href": "/docwhoapi/hal-forms/create-doctor",
            "type": "application/prs.hal-forms+json"
        }
    }
}

A HAL-FORMS documents can be created and encoded to JSON this way:

href := "www.example.com"
document := NewDocument(href)
encoder := halforms.NewEncoder()
bytes, _ := encoder.ToJSON(document) // skipped error handling

Generated JSON

{
    "_links": {
        "self": {
            "href": "www.example.com",
            "type": "application/prs.hal-forms+json"
        }
    },
    "_templates": {}
}

The created document can be enriched with detailed form filed information.

// create a new template describing how to POST a new value to target URI
template := NewTemplate()
template.Method = http.MethodPost
template.Target = "/docwhoapi/doctors"
// create a name property for the form.
property := NewProperty("name")
property.Prompt = "Name"
property.Placeholder = "the doctor's name"
property.Required = true

template.Properties = append(template.Properties, property)
document.AddTemplate(template)

Generated JSON

{
    "_links": {
        "self": {
            "href": "/docwhoapi/hal-forms/create-doctor",
            "type": "application/prs.hal-forms+json"
        }
    },
    "_templates": {
        "default": {
            "contentType": "application/json",
            "key": "default",
            "method": "POST",
            "properties": [
                {
                    "name": "name",
                    "prompt": "Name",
                    "readOnly": false,
                    "regex": "",
                    "required": true,
                    "templated": false,
                    "value": "",
                    "cols": 40,
                    "max": "",
                    "maxLength": "",
                    "min": "",
                    "minLength": "",
                    "placeholder": "the doctor's name",
                    "rows": 5,
                    "step": 0,
                    "type": "text"
                }
            ],
            "target": "/docwhoapi/doctors",
            "title": "default"
        }
    }
}

Documentation

See package documentation:

GoDoc

License

go2hal is released under MIT license. See LICENSE.

About

A HAL implementation in Go with JSON generator.

License:MIT License


Languages

Language:Go 100.0%