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.
- 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
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"
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 😄
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"
}
}
}
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.
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"
}
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
}
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.
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"
}
]
},
}
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"
}
}
}
See package documentation:
go2hal
is released under MIT license. See LICENSE.