├── go.mod ├── Dockerfile ├── .gitignore ├── .travis.yml ├── event-handle.go ├── .github └── workflows │ └── go.yml ├── events-handle.go ├── examples ├── simplest-thing │ └── simplest-thing.go ├── single-thing │ └── single-thing.go └── multiple-things │ └── multiple-things.go ├── go.sum ├── utils.go ├── value.go ├── test.sh ├── property-handle.go ├── event.go ├── README.md ├── actions-handle.go ├── action-handle.go ├── action.go ├── property.go ├── server.go ├── LICENSE └── thing.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dravenk/webthing-go 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/google/uuid v1.2.0 7 | github.com/gorilla/websocket v1.4.2 8 | github.com/xeipuuv/gojsonschema v1.2.0 9 | ) 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang 2 | 3 | LABEL MAINTAINER='longxianwen@outlook.com' 4 | 5 | WORKDIR /go/src/github.com/dravenk/webthing-go 6 | COPY . . 7 | 8 | RUN go install examples/single-thing/single-thing.go 9 | 10 | EXPOSE 8888 11 | CMD ["single-thing"] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | vendor/ 16 | 17 | # Ignore test script 18 | webthing-tester -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.11.x 5 | - 1.13.x 6 | 7 | before_install: 8 | - go get -t -v ./... 9 | - go get github.com/mattn/goveralls 10 | - go get golang.org/x/tools/cmd/cover 11 | 12 | script: 13 | - $GOPATH/bin/goveralls -service=travis-ci 14 | - go test ./... -race -coverprofile=coverage.txt -covermode=atomic 15 | 16 | after_success: 17 | - bash <(curl -s https://codecov.io/bash) 18 | 19 | notifications: 20 | email: 21 | recipients: longxianwen@outlook.com 22 | on_success: change 23 | on_failure: always 24 | -------------------------------------------------------------------------------- /event-handle.go: -------------------------------------------------------------------------------- 1 | package webthing 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | // EventHandle handle a request to /events. 9 | type EventHandle struct { 10 | *EventsHandle 11 | eventName string 12 | } 13 | 14 | // Handle a request to /events. 15 | func (h *EventHandle) Handle(w http.ResponseWriter, r *http.Request) { 16 | BaseHandle(h, w, r) 17 | } 18 | 19 | // Get Handle a GET request. 20 | // 21 | // @param {Object} r The request object 22 | // @param {Object} w The response object 23 | func (h *EventHandle) Get(w http.ResponseWriter, r *http.Request) { 24 | if content := h.Thing.EventDescriptions(h.eventName); content != nil { 25 | if _, err := w.Write(content); err != nil { 26 | fmt.Print(err) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v3 18 | with: 19 | go-version: 1.17 20 | 21 | - name: Build 22 | #run: go build -v ./... 23 | run: go install 24 | 25 | - name: Test 26 | #run: go test -v ./... 27 | run: | 28 | go test -v -cover ./... -coverprofile coverage.out -coverpkg ./... 29 | go tool cover -func coverage.out -o coverage.out # Replaces coverage.out with the analysis of coverage.out 30 | 31 | - name: Check Server API 32 | run: ./test.sh 33 | -------------------------------------------------------------------------------- /events-handle.go: -------------------------------------------------------------------------------- 1 | package webthing 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | // EventsHandle a request to /events. 9 | type EventsHandle struct { 10 | *ThingHandle 11 | } 12 | 13 | // Handle a request to /events. 14 | func (h *EventsHandle) Handle(w http.ResponseWriter, r *http.Request) { 15 | if name, err := resource(r.RequestURI); err == nil { 16 | eventHandle := &EventHandle{h, name} 17 | eventHandle.Handle(w, r) 18 | return 19 | } 20 | BaseHandle(h, w, r) 21 | } 22 | 23 | // Get Handle a GET request. 24 | // 25 | // @param {Object} r The request object 26 | // @param {Object} w The response object 27 | func (h *EventsHandle) Get(w http.ResponseWriter, r *http.Request) { 28 | if content := h.Thing.EventDescriptions(""); content != nil { 29 | if _, err := w.Write(content); err != nil { 30 | fmt.Println(err) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/simplest-thing/simplest-thing.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/dravenk/webthing-go" 9 | ) 10 | 11 | func main() { 12 | runServer() 13 | } 14 | 15 | func makeThing() *webthing.Thing { 16 | thing := webthing.NewThing("urn:dev:ops:my-actuator-1234", 17 | "ActuatorExample", 18 | []string{"OnOffSwitch"}, 19 | "An actuator example that just log") 20 | 21 | value := webthing.NewValue(true, func(i interface{}) { 22 | fmt.Println("Change: ", i) 23 | }) 24 | meta := []byte(`{ 25 | '@type': 'OnOffProperty', 26 | title: 'On/Off', 27 | type: 'boolean', 28 | description: 'Whether the output is changed', 29 | }`) 30 | property := webthing.NewProperty(thing, "on", value, meta) 31 | thing.AddProperty(property) 32 | 33 | return thing 34 | 35 | } 36 | 37 | func runServer() { 38 | fmt.Println("Usage:\n" + 39 | "Try: \n " + 40 | `curl -X PUT -H 'Content-Type: application/json' --data '{"on": true }' ` + ` http://localhost:8888/things/0/properties/on`) 41 | 42 | thing := makeThing() 43 | serve := &http.Server{Addr: ":8888"} 44 | server := webthing.NewWebThingServer(webthing.NewSingleThing(thing), serve, "/things") 45 | 46 | log.Fatal(server.Start()) 47 | } 48 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= 3 | github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 4 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 5 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 8 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 9 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= 10 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 11 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= 12 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= 13 | github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= 14 | github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= 15 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package webthing 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "regexp" 7 | "time" 8 | ) 9 | 10 | // Timestamp Get the current time. 11 | // 12 | // @return The current time in the form YYYY-mm-ddTHH:MM:SS+00.00 13 | func Timestamp() string { 14 | now := time.Now().UTC().Format("2006-01-02T15:04:05") 15 | return now + "+00:00" 16 | } 17 | 18 | func trimSlash(path string) string { 19 | l := len(path) 20 | if l != 1 && path[l-1:] == "/" { 21 | return path[:l-1] 22 | } 23 | return path 24 | } 25 | 26 | func resource(path string) (string, error) { 27 | m := validPath().FindStringSubmatch(path) 28 | if m == nil { 29 | return "", errors.New(" Invalid path! ") 30 | } 31 | return m[2], nil // The resource is the second subexpression. 32 | } 33 | 34 | func validPath() *regexp.Regexp { 35 | // return regexp.MustCompile(`\/(properties|actions|events)\/([a-zA-Z0-9]+)$`) 36 | return regexp.MustCompile(`(properties|actions|events)\/([a-zA-Z0-9]+)`) 37 | } 38 | 39 | // corsResponse Add necessary CORS headers to response. 40 | // 41 | // @param response Response to add headers to 42 | // @return The Response object. 43 | func corsResponse(w http.ResponseWriter) http.ResponseWriter { 44 | w.Header().Set("Access-Control-Allow-Origin", "*") 45 | w.Header().Set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept") 46 | w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, PUT, POST, DELETE") 47 | return w 48 | } 49 | 50 | //jsonResponse Add json headers to response. 51 | func jsonResponse(w http.ResponseWriter) http.ResponseWriter { 52 | w.Header().Set("Content-Type", "application/json") 53 | return w 54 | } 55 | -------------------------------------------------------------------------------- /value.go: -------------------------------------------------------------------------------- 1 | package webthing 2 | 3 | // Value A property value. 4 | // 5 | // This is used for communicating between the Thing representation and the 6 | // actual physical thing implementation. 7 | // 8 | // Notifies all observers when the underlying value changes through an external 9 | // update (command to turn the light off) or if the underlying sensor reports a 10 | // new value. 11 | type Value struct { 12 | lastValue interface{} 13 | valueForwarder []func(interface{}) 14 | } 15 | 16 | // NewValue Initialize the object. 17 | // 18 | // @param {*} initialValue The initial value 19 | // @param {function?} valueForwarder The method that updates the actual value 20 | // on the thing 21 | func NewValue(initialValue interface{}, valueForwarder ...func(interface{})) Value { 22 | return Value{initialValue, valueForwarder} 23 | } 24 | 25 | // Set a new value for this thing. 26 | // 27 | // @param {*} value Value to set 28 | func (v *Value) Set(value interface{}) { 29 | if v.valueForwarder != nil { 30 | for _, valueForwarder := range v.valueForwarder { 31 | valueForwarder(value) 32 | } 33 | } 34 | 35 | v.NotifyOfExternalUpdate(value) 36 | } 37 | 38 | // Get Return the last known value from the underlying thing. 39 | // 40 | // @returns the value. 41 | func (v *Value) Get() interface{} { 42 | return v.lastValue 43 | } 44 | 45 | // NotifyOfExternalUpdate Notify observers of a new value. 46 | // 47 | // @param {*} value New value 48 | func (v *Value) NotifyOfExternalUpdate(value interface{}) { 49 | if value != nil && value != v.lastValue { 50 | v.lastValue = value 51 | //v.emit('update', value); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # clone the webthing-tester 4 | if [ ! -d webthing-tester ]; then 5 | git clone https://github.com/WebThingsIO/webthing-tester 6 | fi 7 | pip3 install --user -r webthing-tester/requirements.txt 8 | 9 | # build and test the single-thing example 10 | 11 | # go run examples/single-thing/single-thing.go & EXAMPLE_PID=$! 12 | # EXAMPLE_PID = go pid, not single-thing pid 13 | 14 | go run examples/single-thing/single-thing.go >/dev/null 2>&1 & 15 | 16 | sleep 5 17 | 18 | function get_pid_by_listened_port() { 19 | pattern_str="*:8888" 20 | pid=$(ss -n -t -l -p | grep "$pattern_str" | column -t | awk -F ',' '{print $(NF-1)}') 21 | [[ $pid =~ "pid" ]] && pid=$(echo $pid | awk -F '=' '{print $NF}') 22 | EXAMPLE_PID=$pid 23 | # echo EXAMPLE_PID 24 | } 25 | get_pid_by_listened_port 26 | 27 | ./webthing-tester/test-client.py --skip-websocket --debug || ! echo 'Test failed' ; killall -9 single-thing 28 | # ./webthing-tester/test-client.py --skip-websocket || ! echo 'Test failed' ; killall -9 single-thing 29 | 30 | echo "single-thing test done!" 31 | # kill -9 $EXAMPLE_PID 32 | 33 | 34 | # build and test the multiple-things example 35 | # ignore all print to std. >/dev/null 2>&1 & 36 | go run examples/multiple-things/multiple-things.go > /dev/null 2>&1 & 37 | sleep 5 38 | get_pid_by_listened_port 39 | 40 | # ignore test result and kill process 41 | ./webthing-tester/test-client.py --path-prefix "/0" --skip-websocket --debug || ! echo 'Test failed' ; killall -9 multiple-things 42 | # ./webthing-tester/test-client.py --path-prefix "/0" --skip-websocket || ! echo 'Test failed' ; killall -9 multiple-things 43 | 44 | killall -9 multiple-things 45 | # kill -9 $EXAMPLE_PID -------------------------------------------------------------------------------- /property-handle.go: -------------------------------------------------------------------------------- 1 | package webthing 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | ) 9 | 10 | // PropertyHandle a request to /properties/. 11 | type PropertyHandle struct { 12 | *PropertiesHandle 13 | *Property 14 | } 15 | 16 | // Handle a request to /properties/. 17 | func (h *PropertyHandle) Handle(w http.ResponseWriter, r *http.Request) { 18 | name, err := resource(trimSlash(r.RequestURI)) 19 | if err != nil { 20 | w.WriteHeader(http.StatusBadRequest) 21 | return 22 | } 23 | h.Property = h.properties[name] 24 | BaseHandle(h, w, r) 25 | } 26 | 27 | // Get Handle a GET request. 28 | // 29 | // @param {Object} r The request object 30 | // @param {Object} w The response object 31 | func (h *PropertyHandle) Get(w http.ResponseWriter, r *http.Request) { 32 | 33 | name := h.Property.Name() 34 | value := h.Property.Value().Get() 35 | description := make(map[string]interface{}) 36 | description[name] = value 37 | 38 | content, err := json.Marshal(description) 39 | if err != nil { 40 | fmt.Println(err) 41 | } 42 | if _, err := w.Write(content); err != nil { 43 | fmt.Println(err) 44 | } 45 | } 46 | 47 | // Put Handle a PUT request. 48 | // 49 | // @param {Object} r The request object 50 | // @param {Object} w The response object 51 | func (h *PropertyHandle) Put(w http.ResponseWriter, r *http.Request) { 52 | 53 | body, _ := ioutil.ReadAll(r.Body) 54 | 55 | var obj map[string]interface{} 56 | err := json.Unmarshal(body, &obj) 57 | if err != nil { 58 | fmt.Println(err) 59 | w.WriteHeader(http.StatusBadRequest) 60 | return 61 | } 62 | 63 | name := h.Property.Name() 64 | h.Property.Value().Set(obj[name]) 65 | 66 | description := make(map[string]interface{}) 67 | description[name] = h.Property.Value().Get() 68 | content, _ := json.Marshal(description) 69 | 70 | if _, err = w.Write(content); err != nil { 71 | fmt.Println(err) 72 | w.WriteHeader(http.StatusBadRequest) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /event.go: -------------------------------------------------------------------------------- 1 | package webthing 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // Event An Event represents an individual event from a thing. 9 | type Event struct { 10 | thing *Thing 11 | name string 12 | data json.RawMessage 13 | time string 14 | } 15 | 16 | // EventObject An event object describes a kind of event which may be emitted by a device. 17 | // See https://iot.mozilla.org/wot/#event-object 18 | type EventObject struct { 19 | AtType string `json:"@type,omitempty"` 20 | Title string `json:"title,omitempty"` 21 | ObjectType string `json:"type,omitempty"` 22 | Description string `json:"description,omitempty"` 23 | Unit string `json:"unit,omitempty"` 24 | Links []Link `json:"links,omitempty"` 25 | } 26 | 27 | // NewEvent Initialize the object. 28 | // @param thing Thing this event belongs to 29 | // @param name Name of the event 30 | // @param data Data associated with the event 31 | func NewEvent(thing *Thing, name string, data json.RawMessage) *Event { 32 | return &Event{ 33 | thing: thing, 34 | name: name, 35 | data: data, 36 | time: Timestamp(), 37 | } 38 | } 39 | 40 | // AsEventDescription Get the event description. 41 | // @return Description of the event as a JSONObject. 42 | func (event *Event) AsEventDescription() []byte { 43 | eve := struct { 44 | Timestamp string `json:"timestamp"` 45 | Data json.RawMessage `json:"data,omitempty"` 46 | }{ 47 | Timestamp: event.Time(), 48 | Data: event.Data(), 49 | } 50 | base := make(map[string]interface{}) 51 | base[event.Name()] = eve 52 | 53 | description, err := json.Marshal(base) 54 | if err != nil { 55 | fmt.Println("Error:", err.Error()) 56 | } 57 | 58 | return description 59 | 60 | } 61 | 62 | // Thing Get the thing associated with this event. 63 | // @returns {Object} The thing. 64 | func (event *Event) Thing() *Thing { 65 | return event.thing 66 | } 67 | 68 | // Name Get the event's name. 69 | // @returns {String} The name. 70 | func (event *Event) Name() string { 71 | return event.name 72 | } 73 | 74 | // Data Get the event's data. 75 | // @returns {*} The data. 76 | func (event *Event) Data() json.RawMessage { 77 | return event.data 78 | } 79 | 80 | // Time Get the event's timestamp. 81 | // @returns {String} The time. 82 | func (event *Event) Time() string { 83 | return event.time 84 | } 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Web of Things 2 | 3 | --- 4 | 5 | [![GitHub forks](https://img.shields.io/github/forks/dravenk/webthing-go.svg?style=social&label=Fork&maxAge=2592000)](https://GitHub.com/dravenk/webthing-go/network/) 6 | [![GitHub version](https://badge.fury.io/gh/dravenk%2Fwebthing-go.svg)](https://badge.fury.io/gh/dravenk%2Fwebthing-go) 7 | [![GoDoc](https://godoc.org/github.com/dravenk/webthing-go?status.png)](https://godoc.org/github.com/dravenk/webthing-go) 8 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/bef38274a3cb4156b374bb76dc1670e5)](https://www.codacy.com/manual/dravenk/webthing-go?utm_source=github.com&utm_medium=referral&utm_content=dravenk/webthing-go&utm_campaign=Badge_Grade) 9 | [![travis](https://api.travis-ci.com/dravenk/webthing-go.svg)](https://api.travis-ci.com/dravenk/webthing-go.svg) 10 | [![Go Report Card](https://goreportcard.com/badge/github.com/dravenk/webthing-go)](https://goreportcard.com/report/github.com/dravenk/webthing-go) 11 | [![codebeat badge](https://codebeat.co/badges/090b9189-b20c-4910-8ff2-d7c12a28e55f)](https://codebeat.co/projects/github-com-dravenk-webthing-go-master) 12 | [![Build Status](https://img.shields.io/docker/cloud/build/dravenk/webthing.svg)](https://cloud.docker.com/repository/docker/dravenk/webthing/builds) 13 | 14 | ## USAGE 15 | 16 | This library fully supports [Web Thing REST API](https://webthings.io/api/#web-thing-rest-api).You can start building your Web of Thing by looking at [single-thing](https://github.com/dravenk/webthing-go/blob/master/examples/single-thing/single-thing.go). 17 | 18 | ### Download and import 19 | 20 | This package name is called `webthing`. This project is called webthing-go to keep the naming consistent with the implementation of other languages. You just need to import this package the way golang normally imports a package. 21 | 22 | ```shell 23 | go get -u -v github.com/dravenk/webthing-go 24 | ``` 25 | 26 | ```go 27 | import ( 28 | "github.com/dravenk/webthing-go" 29 | ) 30 | ``` 31 | 32 | #### Create Thing 33 | 34 | ```go 35 | // Create a Lamp. 36 | thing := webthing.NewThing("urn:dev:ops:my-thing-1234", 37 | "Lamp", 38 | []string{"OnOffSwitch", "Light"}, 39 | "A web connected thing") 40 | ``` 41 | 42 | For more information on Creating Webthing, please check the wiki [Create-Thing](https://github.com/dravenk/webthing-go/wiki/Create-Thing) 43 | 44 | #### Example 45 | 46 | ```shell 47 | cd $GOPATH/src/github.com/dravenk/webthing-go 48 | go run examples/single-thing/single-thing.go 49 | ``` 50 | 51 | You can also run a sample with [docker](https://hub.docker.com/r/dravenk/webthing): 52 | 53 | ```shell 54 | docker run -ti --name single-thing -p 8888:8888 dravenk/webthing 55 | ``` 56 | 57 | For more information on Run Example, please check the wiki [Run-Example](https://github.com/dravenk/webthing-go/wiki/Run-example) 58 | 59 | RESOURCES 60 | 61 | * 62 | * 63 | -------------------------------------------------------------------------------- /actions-handle.go: -------------------------------------------------------------------------------- 1 | package webthing 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "regexp" 10 | ) 11 | 12 | // ActionsHandle Handle a request to /actions. 13 | type ActionsHandle struct { 14 | *ThingHandle 15 | } 16 | 17 | // Handle a request to /actions. 18 | func (h *ActionsHandle) Handle(w http.ResponseWriter, r *http.Request) { 19 | if name, actionID, err := h.matchActionOrID(trimSlash(r.RequestURI)); err == nil { 20 | action := h.Thing.Action(name, actionID) 21 | actionHandle := &ActionHandle{h, name} 22 | if actionID != "" { 23 | actionIDHandle := &ActionIDHandle{actionHandle, action} 24 | actionIDHandle.Handle(w, r) 25 | return 26 | } 27 | actionHandle.Handle(w, r) 28 | return 29 | } 30 | BaseHandle(h, w, r) 31 | } 32 | 33 | func (h *ActionsHandle) matchActionOrID(path string) (actionName, actionID string, err error) { 34 | re := regexp.MustCompile(`^/actions/(.*)/(.*)`) 35 | name := re.FindStringSubmatch(path) 36 | if name != nil { 37 | return name[1], name[2], nil 38 | } 39 | m := validPath().FindStringSubmatch(path) 40 | if m == nil { 41 | return "", "", errors.New(" Invalid! ") 42 | } 43 | return m[2], "", nil // The resource is the second subexpression. 44 | } 45 | 46 | // Get Handle a GET request. 47 | // 48 | // @param {Object} r The request object 49 | // @param {Object} w The response object 50 | func (h *ActionsHandle) Get(w http.ResponseWriter, r *http.Request) { 51 | var description []json.RawMessage 52 | 53 | for name := range h.Thing.actions { 54 | if content := h.Thing.ActionDescriptions(name); content != nil { 55 | description = append(description, content...) 56 | } 57 | } 58 | if len(description) == 0 { 59 | if _, err := w.Write([]byte(`{}`)); err != nil { 60 | fmt.Println(err) 61 | } 62 | return 63 | } 64 | 65 | content, _ := json.Marshal(description) 66 | if _, err := w.Write(content); err != nil { 67 | fmt.Println(err) 68 | } 69 | } 70 | 71 | // Post Handle a POST request. 72 | // 73 | // @param {Object} req The request object 74 | // @param {Object} res The response object 75 | func (h *ActionsHandle) Post(w http.ResponseWriter, r *http.Request) { 76 | body, _ := ioutil.ReadAll(r.Body) 77 | 78 | var obj map[string]map[string]*json.RawMessage 79 | err := json.Unmarshal(body, &obj) 80 | if err != nil { 81 | fmt.Println(err) 82 | w.WriteHeader(http.StatusBadRequest) 83 | return 84 | } 85 | 86 | var description []json.RawMessage 87 | for name, params := range obj { 88 | if _, ok := h.Thing.actions[name]; ok { 89 | input := params["input"] 90 | action, err := h.Thing.PerformAction(name, input) 91 | 92 | if action == nil || err != nil { 93 | fmt.Println("PerformAction failed! The action name is: ", name) 94 | w.WriteHeader(http.StatusBadRequest) 95 | // w.Write([]byte(err.Error())) 96 | return 97 | } 98 | 99 | // Perform an Action in a goroutine. 100 | go action.Start() 101 | 102 | if len(obj) == 1 { 103 | w.WriteHeader(http.StatusCreated) 104 | w.Write(action.AsActionDescription()) 105 | return 106 | } 107 | description = append(description, action.AsActionDescription()) 108 | } 109 | } 110 | content, _ := json.Marshal(description) 111 | w.Write(content) 112 | } 113 | -------------------------------------------------------------------------------- /action-handle.go: -------------------------------------------------------------------------------- 1 | package webthing 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | ) 9 | 10 | // ActionHandle Handle a request to /actions/. 11 | type ActionHandle struct { 12 | *ActionsHandle 13 | ActionName string 14 | } 15 | 16 | // Handle a request to /actions/. 17 | func (h *ActionHandle) Handle(w http.ResponseWriter, r *http.Request) { 18 | BaseHandle(h, w, r) 19 | } 20 | 21 | // Get Handle a GET request. 22 | // 23 | // @param {Object} r The request object 24 | // @param {Object} w The response object 25 | func (h *ActionHandle) Get(w http.ResponseWriter, r *http.Request) { 26 | if descriptions := h.Thing.ActionDescriptions(h.ActionName); len(descriptions) > 0 { 27 | content, _ := json.Marshal(descriptions) 28 | if _, err := w.Write(content); err != nil { 29 | fmt.Println(err) 30 | } 31 | } 32 | } 33 | 34 | // Post Handle a Post request. 35 | // 36 | // @param {Object} r The request object 37 | // @param {Object} w The response object 38 | func (h *ActionHandle) Post(w http.ResponseWriter, r *http.Request) { 39 | handleActionPost(h.Thing, w, r) 40 | } 41 | 42 | func handleActionPost(th *Thing, w http.ResponseWriter, r *http.Request) { 43 | body, _ := ioutil.ReadAll(r.Body) 44 | 45 | var obj map[string]map[string]*json.RawMessage 46 | err := json.Unmarshal(body, &obj) 47 | if err != nil { 48 | fmt.Println(err) 49 | w.WriteHeader(http.StatusBadRequest) 50 | return 51 | } 52 | 53 | var description []json.RawMessage 54 | for name, params := range obj { 55 | if _, ok := th.actions[name]; ok { 56 | input := params["input"] 57 | action, err := th.PerformAction(name, input) 58 | 59 | if action == nil || err != nil { 60 | fmt.Println("PerformAction failed! The action name is: ", name) 61 | w.WriteHeader(http.StatusBadRequest) 62 | // w.Write([]byte(err.Error())) 63 | return 64 | } 65 | 66 | // Perform an Action in a goroutine. 67 | go action.Start() 68 | 69 | if len(obj) == 1 { 70 | w.WriteHeader(http.StatusCreated) 71 | w.Write(action.AsActionDescription()) 72 | return 73 | } 74 | 75 | description = append(description, action.AsActionDescription()) 76 | } 77 | } 78 | 79 | content, _ := json.Marshal(description) 80 | w.WriteHeader(http.StatusCreated) 81 | w.Write(content) 82 | } 83 | 84 | // ActionIDHandle Handle a request to /actions//. 85 | type ActionIDHandle struct { 86 | *ActionHandle 87 | *Action 88 | } 89 | 90 | // Handle a request to /actions//. 91 | func (h *ActionIDHandle) Handle(w http.ResponseWriter, r *http.Request) { 92 | if h.Action == nil { 93 | w.WriteHeader(http.StatusBadRequest) 94 | if _, err := w.Write([]byte(`Bad request. Action not found.`)); err != nil { 95 | fmt.Print(err) 96 | } 97 | return 98 | } 99 | BaseHandle(h, w, r) 100 | } 101 | 102 | // Get Handle a GET request. 103 | // 104 | // @param {Object} r The request object 105 | // @param {Object} w The response object 106 | func (h *ActionIDHandle) Get(w http.ResponseWriter, r *http.Request) { 107 | if _, err := w.Write(h.Action.AsActionDescription()); err != nil { 108 | fmt.Println(err) 109 | } 110 | } 111 | 112 | // Delete Handle a Delete request. 113 | // 114 | // @param {Object} r The request object 115 | // @param {Object} w The response object 116 | func (h *ActionIDHandle) Delete(w http.ResponseWriter, r *http.Request) { 117 | if h.RemoveAction(h.ActionName, h.Action.ID()) { 118 | w.WriteHeader(http.StatusNoContent) 119 | return 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /examples/single-thing/single-thing.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "math/rand" 8 | "net/http" 9 | "time" 10 | 11 | "github.com/dravenk/webthing-go" 12 | "github.com/google/uuid" 13 | ) 14 | 15 | func main() { 16 | 17 | thing := MakeThing() 18 | 19 | singleThing := webthing.NewSingleThing(thing) 20 | httpServer := &http.Server{Addr: "0.0.0.0:8888"} 21 | 22 | server := webthing.NewWebThingServer(singleThing, httpServer, "") 23 | log.Fatal(server.Start()) 24 | } 25 | 26 | func MakeThing() *webthing.Thing { 27 | // Create a Lamp. 28 | thing := webthing.NewThing("urn:dev:ops:my-lamp-1234", 29 | "My Lamp", 30 | []string{"OnOffSwitch", "Light"}, 31 | "A web connected lamp") 32 | 33 | // Adding an OnOffProperty to thing. 34 | onDescription := []byte(`{ 35 | "@type": "OnOffProperty", 36 | "type": "boolean", 37 | "title": "On/Off", 38 | "description": "Whether the lamp is turned on" 39 | }`) 40 | onValue := webthing.NewValue(true, onValueForwarder) 41 | on := webthing.NewProperty(thing, 42 | "on", 43 | onValue, 44 | onDescription) 45 | thing.AddProperty(on) 46 | 47 | // Adding an BrightnessProperty to this Lamp. 48 | brightnessDescription := []byte(`{ 49 | "@type": "BrightnessProperty", 50 | "type": "integer", 51 | "title": "Brightness", 52 | "description": "The level of light from 0-100", 53 | "minimum": 0, 54 | "maximum": 100, 55 | "unit": "percent" 56 | }`) 57 | 58 | thing.AddProperty(webthing.NewProperty(thing, 59 | "brightness", 60 | webthing.NewValue(50), 61 | brightnessDescription)) 62 | 63 | //Adding a Fade action to this Lamp. 64 | fadeMeta := []byte(`{ 65 | "title": "Fade", 66 | "description": "Fade the lamp to a given level", 67 | "input": { 68 | "@type": "FadeAction", 69 | "type": "object", 70 | "required": [ 71 | "brightness", 72 | "duration" 73 | ], 74 | "properties": { 75 | "brightness": { 76 | "type": "integer", 77 | "minimum": 0, 78 | "maximum": 100, 79 | "unit": "percent" 80 | }, 81 | "duration": { 82 | "type": "integer", 83 | "minimum": 1, 84 | "unit": "milliseconds" 85 | } 86 | } 87 | } 88 | }`) 89 | fade := &FadeAction{} 90 | thing.AddAvailableAction("fade", fadeMeta, fade) 91 | 92 | thing.AddAvailableEvent("overheated", 93 | []byte(`{ 94 | "description": 95 | "The lamp has exceeded its safe operating temperature", 96 | "type": "number", 97 | "unit": "degree celsius" 98 | }`)) 99 | 100 | toggleMeta := []byte(`{ 101 | "title": "Toggle", 102 | "description": "Toggles a boolean state on and off." 103 | }`) 104 | toggle := &ToggleAction{} 105 | thing.AddAvailableAction("toggle", toggleMeta, toggle) 106 | 107 | return thing 108 | } 109 | 110 | // Fade the lamp to a given brightness 111 | type FadeAction struct { 112 | *webthing.Action 113 | } 114 | 115 | func (fade *FadeAction) Generator(thing *webthing.Thing) *webthing.Action { 116 | fade.Action = webthing.NewAction(uuid.New().String(), thing, "fade", nil, fade.PerformAction, fade.Cancel) 117 | return fade.Action 118 | } 119 | 120 | func (fade *FadeAction) PerformAction() *webthing.Action { 121 | fmt.Println("Perform fade action…...: ", fade.Name(), " | UUID: ", fade.ID()) 122 | thing := fade.Thing() 123 | params, _ := fade.Input().MarshalJSON() 124 | 125 | input := make(map[string]interface{}) 126 | if err := json.Unmarshal(params, &input); err != nil { 127 | fmt.Println(err) 128 | } 129 | if brightness, ok := input["brightness"]; ok { 130 | fmt.Println("Set brightness value: ", brightness) 131 | thing.Property("brightness").Set(brightness) 132 | } 133 | if duration, ok := input["duration"]; ok { 134 | fmt.Println("Fade duration: ", duration) 135 | time.Sleep(time.Duration(int64(duration.(float64))) * time.Millisecond) 136 | } 137 | 138 | event := webthing.NewEvent(thing, "overheated", []byte(fmt.Sprintln(102))) 139 | thing.AddEvent(event) 140 | 141 | fmt.Println("Fade action Done...", fade.Name()) 142 | return fade.Action 143 | } 144 | 145 | func (fade *FadeAction) Cancel() { 146 | fmt.Println("Cancel fade action...") 147 | } 148 | 149 | // Toggles a boolean state on and off. 150 | // Customize a toggles to control the on-off state 151 | type ToggleAction struct { 152 | *webthing.Action 153 | } 154 | 155 | func (toggle *ToggleAction) Generator(thing *webthing.Thing) *webthing.Action { 156 | toggle.Action = webthing.NewAction(uuid.New().String(), thing, "toggle", nil, toggle.PerformAction, toggle.Cancel) 157 | return toggle.Action 158 | } 159 | 160 | func (toggle *ToggleAction) PerformAction() *webthing.Action { 161 | fmt.Println("Perform toggle action...: ", toggle.Name(), " | UUID: ", toggle.ID()) 162 | 163 | thing := toggle.Thing() 164 | property := thing.Property("on") 165 | on := property.Get().(bool) 166 | property.Set(!on) 167 | 168 | event := webthing.NewEvent(thing, "overheated", []byte(fmt.Sprintln(rand.Intn(100)))) 169 | thing.AddEvent(event) 170 | 171 | fmt.Println("Toggle action done...") 172 | return toggle.Action 173 | } 174 | 175 | func (toggle *ToggleAction) Cancel() { 176 | fmt.Println("Cancel toggle action...") 177 | } 178 | 179 | func onValueForwarder(i interface{}) { 180 | fmt.Println("Now on statue: ", i) 181 | } 182 | -------------------------------------------------------------------------------- /action.go: -------------------------------------------------------------------------------- 1 | package webthing 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // Action An Action represents an individual action on a thing. 9 | type Action struct { 10 | id string 11 | thing *Thing 12 | name string 13 | input *json.RawMessage 14 | hrefPrefix string 15 | href string 16 | status string 17 | timeRequested string 18 | timeCompleted string 19 | 20 | // Override this with the code necessary to perform the action. 21 | PerformAction func() *Action 22 | 23 | // Override this with the code necessary to cancel the action. 24 | Cancel func() 25 | } 26 | 27 | // Actioner Customize the methods that the action must implement. 28 | type Actioner interface { 29 | // Custom Action need create a Generator to generate a action. 30 | // The application will invoke the Action created by the Generator method. 31 | // This is very similar to simply constructor. 32 | // See thing.PerformAction()*Action 33 | Generator(thing *Thing) *Action 34 | 35 | // Override this with the code necessary to perform the action. 36 | PerformAction() *Action 37 | 38 | // Override this with the code necessary to cancel the action. 39 | Cancel() 40 | 41 | Start() *Action 42 | Finish() *Action 43 | 44 | AsActionDescription() []byte 45 | SetHrefPrefix(prefix string) 46 | ID() string 47 | Name() string 48 | Href() string 49 | Status() string 50 | Thing() *Thing 51 | TimeRequested() string 52 | TimeCompleted() string 53 | Input() *json.RawMessage 54 | SetInput(input *json.RawMessage) 55 | } 56 | 57 | // NewAction Initialize the object. 58 | // @param id ID of this action 59 | // @param thing Thing this action belongs to 60 | // @param name Name of the action 61 | // @param input Any action inputs 62 | func NewAction(id string, thing *Thing, name string, input *json.RawMessage, PerformAction func() *Action, Cancel func()) *Action { 63 | action := &Action{ 64 | id: id, 65 | thing: thing, 66 | name: name, 67 | hrefPrefix: "", 68 | href: fmt.Sprintf("/actions/%s/%s", name, id), 69 | status: "created", 70 | timeRequested: Timestamp(), 71 | PerformAction: PerformAction, 72 | Cancel: Cancel, 73 | } 74 | if input != nil { 75 | action.input = input 76 | } 77 | 78 | return action 79 | } 80 | 81 | // AsActionDescription Get the action description. 82 | // @return Description of the action as a JSONObject. 83 | func (action *Action) AsActionDescription() []byte { 84 | actionName := action.Name() 85 | obj := make(map[string]interface{}) 86 | actionObj := make(map[string]interface{}) 87 | 88 | if input := action.Input(); input != nil { 89 | actionObj["input"] = input 90 | } 91 | if timeCompleted := action.TimeCompleted(); timeCompleted != "" { 92 | actionObj["timeCompleted"] = timeCompleted 93 | } 94 | actionObj["href"] = action.Href() 95 | actionObj["status"] = action.Status() 96 | actionObj["timeRequested"] = action.TimeRequested() 97 | obj[actionName] = actionObj 98 | 99 | description, err := json.Marshal(obj) 100 | if err != nil { 101 | fmt.Println("Error: ", err.Error()) 102 | return []byte(err.Error()) 103 | } 104 | 105 | return description 106 | } 107 | 108 | // SetHrefPrefix Set the prefix of any hrefs associated with this action. 109 | // @param prefix The prefix 110 | func (action *Action) SetHrefPrefix(prefix string) { 111 | action.hrefPrefix = prefix 112 | } 113 | 114 | // ID Get this action's ID. 115 | // @returns {String} The ID. 116 | func (action *Action) ID() string { 117 | return action.id 118 | } 119 | 120 | // Name Get this action's name. 121 | // @returns {String} The name. 122 | func (action *Action) Name() string { 123 | return action.name 124 | } 125 | 126 | // Href Get this action's href. 127 | // @returns {String} The href. 128 | func (action *Action) Href() string { 129 | return action.hrefPrefix + action.href 130 | } 131 | 132 | // Status Get this action's status. 133 | // Get this action's status. 134 | // @returns {String} The status. 135 | func (action *Action) Status() string { 136 | return action.status 137 | } 138 | 139 | // Thing Get the thing associated with this action. 140 | // @returns {Object} The thing. 141 | func (action *Action) Thing() *Thing { 142 | return action.thing 143 | } 144 | 145 | // TimeRequested Get the time the action was requested. 146 | // @returns {String} The time. 147 | func (action *Action) TimeRequested() string { 148 | return action.timeRequested 149 | } 150 | 151 | // TimeCompleted Get the time the action was completed. 152 | // @returns {String} The time. 153 | func (action *Action) TimeCompleted() string { 154 | return action.timeCompleted 155 | } 156 | 157 | // Input Get the inputs for this action. 158 | // @returns {Object} The inputs. 159 | func (action *Action) Input() *json.RawMessage { 160 | return action.input 161 | } 162 | 163 | // SetInput Set any input to this action. 164 | // @param input The input 165 | func (action *Action) SetInput(input *json.RawMessage) { 166 | if input != nil { 167 | action.input = input 168 | } 169 | } 170 | 171 | // Start performing the action. 172 | func (action *Action) Start() *Action { 173 | // Todo 174 | // Handle an error performing the action. 175 | defer func() { 176 | if e := recover(); e != nil { 177 | fmt.Println("Perform Action encountered an error") 178 | } 179 | }() 180 | 181 | action.status = "pending" 182 | action.thing.ActionNotify(action) 183 | action.PerformAction() 184 | action.Finish() 185 | 186 | return action 187 | } 188 | 189 | // Finish performing the action. 190 | func (action *Action) Finish() *Action { 191 | action.status = "completed" 192 | action.timeCompleted = Timestamp() 193 | action.thing.ActionNotify(action) 194 | return action 195 | } 196 | -------------------------------------------------------------------------------- /examples/multiple-things/multiple-things.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "math" 8 | "math/rand" 9 | "net/http" 10 | "time" 11 | 12 | "github.com/dravenk/webthing-go" 13 | "github.com/google/uuid" 14 | ) 15 | 16 | func main() { 17 | 18 | light := MakeDimmableLight() 19 | 20 | sensor := FakeGpioHumiditySensor() 21 | 22 | multiple := webthing.NewMultipleThings([]*webthing.Thing{light, sensor}, "LightAndTempDevice") 23 | 24 | httpServer := &http.Server{Addr: "0.0.0.0:8888"} 25 | server := webthing.NewWebThingServer(multiple, httpServer, "") 26 | log.Fatal(server.Start()) 27 | } 28 | 29 | // MakeDimmableLight A dimmable light that logs received commands to stdout. 30 | func MakeDimmableLight() *webthing.Thing { 31 | // Create a Lamp. 32 | thing := webthing.NewThing("urn:dev:ops:my-lamp-1234", 33 | "My Lamp", 34 | []string{"OnOffSwitch", "Light"}, 35 | "A web connected lamp") 36 | 37 | // Adding an OnOffProperty to thing. 38 | onDescription := []byte(`{ 39 | "@type": "OnOffProperty", 40 | "type": "boolean", 41 | "title": "On/Off", 42 | "description": "Whether the lamp is turned on" 43 | }`) 44 | onValue := webthing.NewValue(true, func(i interface{}) { 45 | fmt.Println("On-State is now ", i) 46 | }) 47 | on := webthing.NewProperty(thing, 48 | "on", 49 | onValue, 50 | onDescription) 51 | thing.AddProperty(on) 52 | 53 | // Adding an BrightnessProperty to this Lamp. 54 | brightnessDescription := []byte(`{ 55 | "@type": "BrightnessProperty", 56 | "type": "integer", 57 | "title": "Brightness", 58 | "description": "The level of light from 0-100", 59 | "minimum": 0, 60 | "maximum": 100, 61 | "unit": "percent" 62 | }`) 63 | 64 | thing.AddProperty(webthing.NewProperty(thing, 65 | "brightness", 66 | webthing.NewValue(50, func(i interface{}) { 67 | fmt.Print("Brightness is now ", i) 68 | }), 69 | brightnessDescription)) 70 | 71 | //Adding a Fade action to this Lamp. 72 | fadeMeta := []byte(`{ 73 | "title": "Fade", 74 | "description": "Fade the lamp to a given level", 75 | "input": { 76 | "@type": "FadeAction", 77 | "type": "object", 78 | "required": [ 79 | "brightness", 80 | "duration" 81 | ], 82 | "properties": { 83 | "brightness": { 84 | "type": "integer", 85 | "minimum": 0, 86 | "maximum": 100, 87 | "unit": "percent" 88 | }, 89 | "duration": { 90 | "type": "integer", 91 | "minimum": 1, 92 | "unit": "milliseconds" 93 | } 94 | } 95 | } 96 | }`) 97 | fade := &FadeAction{} 98 | thing.AddAvailableAction("fade", fadeMeta, fade) 99 | 100 | thing.AddAvailableEvent("overheated", 101 | []byte(`{ 102 | "description": 103 | "The lamp has exceeded its safe operating temperature", 104 | "type": "number", 105 | "unit": "degree celsius" 106 | }`)) 107 | 108 | return thing 109 | } 110 | 111 | // Fade the lamp to a given brightness 112 | type FadeAction struct { 113 | *webthing.Action 114 | } 115 | 116 | // Generator Action generate. 117 | func (fade *FadeAction) Generator(thing *webthing.Thing) *webthing.Action { 118 | fade.Action = webthing.NewAction(uuid.New().String(), thing, "fade", nil, fade.PerformAction, fade.Cancel) 119 | return fade.Action 120 | } 121 | 122 | // PerformAction Perform an action. 123 | func (fade *FadeAction) PerformAction() *webthing.Action { 124 | fmt.Println("Perform fade action…...: ", fade.Name(), " | UUID: ", fade.ID()) 125 | thing := fade.Thing() 126 | params, _ := fade.Input().MarshalJSON() 127 | 128 | input := make(map[string]interface{}) 129 | if err := json.Unmarshal(params, &input); err != nil { 130 | fmt.Println("PerformAction error ",err) 131 | return nil 132 | } 133 | if brightness, ok := input["brightness"]; ok { 134 | fmt.Println("Set brightness value: ", brightness) 135 | thing.Property("brightness").Set(brightness) 136 | } 137 | if duration, ok := input["duration"]; ok { 138 | fmt.Println("Fade duration: ", duration) 139 | time.Sleep(time.Duration(int64(duration.(float64))) * time.Millisecond) 140 | } 141 | 142 | event := webthing.NewEvent(thing, "overheated", []byte(fmt.Sprintln(102))) 143 | thing.AddEvent(event) 144 | 145 | fmt.Println("Fade action Done...", fade.Name()) 146 | return fade.Action 147 | } 148 | 149 | func (fade *FadeAction) Cancel() {} 150 | 151 | // FakeGpioHumiditySensor A humidity sensor which updates its measurement every few seconds. 152 | func FakeGpioHumiditySensor() *webthing.Thing { 153 | thing := webthing.NewThing( 154 | "urn:dev:ops:my-humidity-sensor-1234", 155 | "My Humidity Sensor", 156 | []string{"MultiLevelSensor"}, 157 | "A web connected humidity sensor") 158 | 159 | level := webthing.NewValue(0.0) 160 | levelDescription := []byte(`{ 161 | "@type": "LevelProperty", 162 | "title": "Humidity", 163 | "type": "number", 164 | "description": "The current humidity in %", 165 | "minimum": 0, 166 | "maximum": 100, 167 | "unit": "percent", 168 | "readOnly": true 169 | }`) 170 | thing.AddProperty(webthing.NewProperty( 171 | thing, 172 | "level", 173 | level, 174 | levelDescription)) 175 | 176 | go func(level webthing.Value) { 177 | for { 178 | time.Sleep(3000 * time.Millisecond) 179 | newLevel := readFromGPIO() 180 | fmt.Println("setting new humidity level:", newLevel) 181 | level.NotifyOfExternalUpdate(newLevel) 182 | } 183 | }(level) 184 | 185 | return thing 186 | } 187 | 188 | // Mimic an actual sensor updating its reading every couple seconds. 189 | func readFromGPIO() float64 { 190 | return math.Abs(rand.Float64() * 70.0 * 0.5 * rand.NormFloat64()) 191 | } 192 | -------------------------------------------------------------------------------- /property.go: -------------------------------------------------------------------------------- 1 | package webthing 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "go/types" 8 | ) 9 | 10 | // Property Initialize the object. 11 | // 12 | // @param thing Thing this property belongs to 13 | // @param name Name of the property 14 | // @param value Value object to hold the property value 15 | // @param metadata Property metadata, i.e. type, description, unit, etc., as 16 | // a Map 17 | type Property struct { 18 | thing *Thing 19 | name string 20 | value *Value 21 | hrefPrefix string 22 | href string 23 | metadata json.RawMessage 24 | } 25 | 26 | // PropertyObject A property object describes an attribute of a Thing and is indexed by a property id. 27 | // See https://iot.mozilla.org/wot/#property-object 28 | type PropertyObject struct { 29 | AtType string `json:"@type,omitempty"` 30 | Title string `json:"title,omitempty"` 31 | Type string `json:"type,omitempty"` 32 | Description string `json:"description,omitempty"` 33 | Unit string `json:"unit,omitempty"` 34 | ReadOnly bool `json:"readOnly,omitempty"` 35 | Minimum json.Number `json:"minimum,omitempty"` 36 | Maximum json.Number `json:"maximum,omitempty"` 37 | Links []Link `json:"links,omitempty"` 38 | } 39 | 40 | // NewProperty Initialize the object. 41 | // 42 | // @param {Object} thing Thing this property belongs to 43 | // @param {String} name Name of the property 44 | // @param {Value} value Value object to hold the property value 45 | // @param {Object} metadata Property metadata, i.e. type, description, unit, 46 | // etc., as an object. 47 | func NewProperty(thing *Thing, name string, value Value, metadata json.RawMessage) *Property { 48 | property := &Property{ 49 | thing: thing, 50 | name: name, 51 | value: &value, 52 | hrefPrefix: "", 53 | href: `/properties/` + name, 54 | metadata: metadata, 55 | } 56 | 57 | // Add the property change observer to notify the Thing about a property 58 | // change. 59 | //property.Value.on("update", () => property.thing.PropertyNotify(*property)); 60 | thing.PropertyNotify(*property) 61 | 62 | return property 63 | } 64 | 65 | // 66 | // ValidateValue Validate new property value before setting it. 67 | // 68 | // @param {*} value - New value 69 | func (property *Property) ValidateValue(value interface{}) error { 70 | prop := &PropertyObject{} 71 | meta, err := property.Metadata().MarshalJSON() 72 | if err != nil { 73 | return err 74 | } 75 | if err = json.Unmarshal(meta, &prop); err != nil { 76 | return err 77 | } 78 | if prop.ReadOnly { 79 | return errors.New(" Read-only property. ") 80 | } 81 | if !validate(prop.Type, value) { 82 | return errors.New(" Invalid property value. ") 83 | } 84 | 85 | return nil 86 | } 87 | 88 | // AsPropertyDescription Get the property description. 89 | // 90 | // @returns {Object} Description of the property as an object. 91 | func (property *Property) AsPropertyDescription() []byte { 92 | 93 | link := Link{ 94 | Rel: "property", 95 | Href: property.hrefPrefix + property.href, 96 | } 97 | base := &PropertyObject{Links: []Link{link}} 98 | 99 | meta, _ := property.Metadata().MarshalJSON() 100 | if err := json.Unmarshal(meta, base); err != nil { 101 | fmt.Println("Error: ", err.Error()) 102 | return []byte(err.Error()) 103 | } 104 | 105 | propertyBase, _ := json.Marshal(base) 106 | 107 | return propertyBase 108 | } 109 | 110 | // SetHrefPrefix Set the prefix of any hrefs associated with this property. 111 | // 112 | // @param {String} prefix The prefix 113 | func (property *Property) SetHrefPrefix(prefix string) { 114 | property.hrefPrefix = prefix 115 | } 116 | 117 | // Href Get the href of this property. 118 | // 119 | // @returns {String} The href 120 | func (property *Property) Href() string { 121 | return property.hrefPrefix + property.href 122 | } 123 | 124 | // Value Get the current property value. 125 | // 126 | // @returns {*} The current value 127 | func (property *Property) Value() *Value { 128 | return property.value 129 | } 130 | 131 | // SetValue Set the current value of the property. 132 | // 133 | // @param {*} value The value to set 134 | func (property *Property) SetValue(value *Value) error { 135 | if err := property.ValidateValue(value); err != nil { 136 | fmt.Print("Ser property value failure. Err: ", err) 137 | return err 138 | } 139 | property.value = value 140 | return nil 141 | } 142 | 143 | // Name Get the name of this property. 144 | // 145 | // @returns {String} The property name. 146 | func (property *Property) Name() string { 147 | return property.name 148 | } 149 | 150 | // Thing Get the thing associated with this property. 151 | // 152 | // @returns {Object} The thing. 153 | func (property *Property) Thing() *Thing { 154 | return property.thing 155 | } 156 | 157 | // Metadata Get the metadata associated with this property 158 | // 159 | // @returns {Object} The metadata 160 | // 161 | func (property *Property) Metadata() json.RawMessage { 162 | return property.metadata 163 | } 164 | 165 | // validate Custom a simply validate. It is not yet possible to verify all types. 166 | // 167 | // A primitive type (one of null, boolean, object, array, number, integer or string as per [json-schema]) 168 | // See: deihttps://iot.mozilla.org/wot/#property-object 169 | // 170 | // See: https://tools.ietf.org/html/draft-zyp-json-schema-04#section-3.5 171 | // 172 | // 3.5. JSON Schema primitive types 173 | // JSON Schema defines seven primitive types for JSON values: 174 | // array A JSON array. 175 | // boolean A JSON boolean. 176 | // integer A JSON number without a fraction or exponent part. 177 | // number Any JSON number. Number includes integer. 178 | // null The JSON null value. 179 | // object A JSON object. 180 | // string A JSON string. 181 | // 182 | func validate(primitive string, v interface{}) bool { 183 | switch v.(type) { 184 | case types.Array: 185 | if primitive == "array" { 186 | return true 187 | } 188 | case bool: 189 | if primitive == "boolean" { 190 | return true 191 | } 192 | case string: 193 | if primitive == "string" { 194 | return true 195 | } 196 | case int, int8, int16, int32, int64: 197 | if primitive == "integer" || primitive == "number" { 198 | return true 199 | } 200 | case float32, float64: 201 | if primitive == "number" { 202 | return true 203 | } 204 | } 205 | 206 | return false 207 | } 208 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package webthing 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "path/filepath" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | // ThingServer Web Thing Server. 13 | type ThingServer struct { 14 | *http.Server 15 | Things []*Thing 16 | Name string 17 | BasePath string 18 | } 19 | 20 | // NewWebThingServer Initialize the WebThingServer. 21 | // 22 | // @param thingType List of Things managed by this server 23 | // @param basePath Base URL path to use, rather than '/' 24 | // 25 | func NewWebThingServer(thingType ThingsType, httpServer *http.Server, basePath string) *ThingServer { 26 | server := &ThingServer{httpServer, thingType.Things(), thingType.Name(), basePath} 27 | thingsNum := len(server.Things) 28 | 29 | thingsHandle := &ThingsHandle{server.Things, basePath} 30 | http.HandleFunc("/", thingsHandle.Handle) 31 | 32 | if thingsNum == 1 { 33 | thing := server.Things[0] 34 | prePath := strings.TrimRight(server.BasePath+"/"+thing.Title(), "/") 35 | preIdx := strings.TrimRight(server.BasePath, "/") 36 | thing.SetHrefPrefix(preIdx) 37 | thingHandle := &ThingHandle{thing} 38 | propertiesHandle := &PropertiesHandle{thingHandle} 39 | actionsHandle := &ActionsHandle{thingHandle} 40 | eventsHandle := &EventsHandle{thingHandle} 41 | 42 | handlerfuncs(prePath, preIdx, thingHandle, propertiesHandle, actionsHandle, eventsHandle) 43 | return server 44 | } 45 | 46 | for id, thing := range server.Things { 47 | prePath := strings.TrimRight(server.BasePath+"/"+thing.Title(), "/") 48 | preIdx := "/" + strconv.Itoa(id) 49 | thing.SetHrefPrefix(preIdx) 50 | thingHandle := &ThingHandle{thing} 51 | propertiesHandle := &PropertiesHandle{thingHandle} 52 | actionsHandle := &ActionsHandle{thingHandle} 53 | eventsHandle := &EventsHandle{thingHandle} 54 | 55 | handlerfuncs(prePath, preIdx, thingHandle, propertiesHandle, actionsHandle, eventsHandle) 56 | } 57 | 58 | return server 59 | } 60 | 61 | func handlerfuncs(prePath, preIdx string, 62 | thingHandle *ThingHandle, 63 | propertiesHandle *PropertiesHandle, 64 | actionsHandle *ActionsHandle, 65 | eventsHandle *EventsHandle, 66 | ) { 67 | 68 | http.HandleFunc(prePath, thingHandle.Handle) 69 | http.HandleFunc(prePath+"/properties", propertiesHandle.Handle) 70 | http.HandleFunc(prePath+"/properties/", propertiesHandle.Handle) 71 | http.HandleFunc(prePath+"/actions", actionsHandle.Handle) 72 | http.HandleFunc(prePath+"/actions/", actionsHandle.Handle) 73 | http.HandleFunc(prePath+"/events", eventsHandle.Handle) 74 | http.HandleFunc(prePath+"/events/", eventsHandle.Handle) 75 | 76 | http.HandleFunc(preIdx+"/properties", propertiesHandle.Handle) 77 | http.HandleFunc(preIdx+"/properties/", propertiesHandle.Handle) 78 | http.HandleFunc(preIdx+"/actions", actionsHandle.Handle) 79 | http.HandleFunc(preIdx+"/actions/", actionsHandle.Handle) 80 | http.HandleFunc(preIdx+"/events", eventsHandle.Handle) 81 | http.HandleFunc(preIdx+"/events/", eventsHandle.Handle) 82 | 83 | if preIdx != "" { 84 | http.HandleFunc(preIdx, thingHandle.Handle) 85 | } 86 | } 87 | 88 | // Start Start listening for incoming connections. 89 | // 90 | // @return Error on failure to listen on port 91 | func (server *ThingServer) Start() error { 92 | return server.ListenAndServe() 93 | } 94 | 95 | // Stop Stop listening. 96 | func (server *ThingServer) Stop() error { 97 | return server.Close() 98 | } 99 | 100 | // ThingsType Container of Things Type 101 | type ThingsType interface { 102 | 103 | // Thing Get the thing at the given index. 104 | // 105 | // @param idx Index of thing. 106 | // @return The thing, or null. 107 | Thing(idx int) *Thing 108 | 109 | // Things Get the list of things. 110 | // 111 | // @return The list of things. 112 | Things() []*Thing 113 | 114 | // Name Get the mDNS server name. 115 | // 116 | // @return The server name. 117 | Name() string 118 | } 119 | 120 | // SingleThing A container for a single thing. 121 | type SingleThing struct { 122 | thing *Thing 123 | } 124 | 125 | // NewSingleThing Initialize the container. 126 | // 127 | // @param {Object} thing The thing to store 128 | func NewSingleThing(thing *Thing) *SingleThing { 129 | return &SingleThing{thing} 130 | } 131 | 132 | // Thing Get the thing at the given index. 133 | func (st *SingleThing) Thing(idx int) *Thing { 134 | return st.thing 135 | } 136 | 137 | // Things Get the list of things. 138 | func (st *SingleThing) Things() []*Thing { 139 | return []*Thing{st.thing} 140 | } 141 | 142 | // Name Get the mDNS server name. 143 | func (st *SingleThing) Name() string { 144 | return st.thing.title 145 | } 146 | 147 | // MultipleThings A container for multiple things. 148 | type MultipleThings struct { 149 | things []*Thing 150 | name string 151 | } 152 | 153 | // NewMultipleThings Initialize the container. 154 | // 155 | // @param {Object} things The things to store 156 | // @param {String} name The mDNS server name 157 | func NewMultipleThings(things []*Thing, name string) *MultipleThings { 158 | mt := &MultipleThings{ 159 | things: things, 160 | name: name, 161 | } 162 | return mt 163 | } 164 | 165 | // Thing Get the thing at the given index. 166 | // 167 | // @param {Number|String} idx The index 168 | func (mt *MultipleThings) Thing(idx int) *Thing { 169 | return mt.things[idx] 170 | } 171 | 172 | // Things Get the list of things. 173 | func (mt *MultipleThings) Things() []*Thing { 174 | return mt.things 175 | } 176 | 177 | // Name Get the mDNS server name. 178 | func (mt *MultipleThings) Name() string { 179 | return mt.name 180 | } 181 | 182 | // // BaseHandler Base handler that is initialized with a list of things. 183 | // type BaseHandler interface { 184 | // Get(w http.ResponseWriter, r *http.Request) 185 | // Post(w http.ResponseWriter, r *http.Request) 186 | // Put(w http.ResponseWriter, r *http.Request) 187 | // Delete(w http.ResponseWriter, r *http.Request) 188 | // } 189 | 190 | // GetInterface Implementation of http Get menthod. 191 | type GetInterface interface { 192 | Get(w http.ResponseWriter, r *http.Request) 193 | } 194 | 195 | // PostInterface Implementation of http Post menthod. 196 | type PostInterface interface { 197 | Post(w http.ResponseWriter, r *http.Request) 198 | } 199 | 200 | // PutInterface Implementation of http Put menthod. 201 | type PutInterface interface { 202 | Put(w http.ResponseWriter, r *http.Request) 203 | } 204 | 205 | // DeleteInterface Implementation of http Delete menthod. 206 | type DeleteInterface interface { 207 | Delete(w http.ResponseWriter, r *http.Request) 208 | } 209 | 210 | // BaseHandle Base handler that is initialized with a list of things. 211 | // func BaseHandle(h BaseHandler, w http.ResponseWriter, r *http.Request) { 212 | func BaseHandle(h interface{}, w http.ResponseWriter, r *http.Request) { 213 | corsResponse(w) 214 | jsonResponse(w) 215 | switch r.Method { 216 | case http.MethodGet: 217 | if base, ok := h.(GetInterface); ok { 218 | base.Get(w, r) 219 | return 220 | } 221 | w.WriteHeader(http.StatusMethodNotAllowed) 222 | return 223 | case http.MethodPost: 224 | if base, ok := h.(PostInterface); ok { 225 | base.Post(w, r) 226 | return 227 | } 228 | w.WriteHeader(http.StatusMethodNotAllowed) 229 | return 230 | case http.MethodPut: 231 | if base, ok := h.(PutInterface); ok { 232 | base.Put(w, r) 233 | return 234 | } 235 | w.WriteHeader(http.StatusMethodNotAllowed) 236 | return 237 | case http.MethodDelete: 238 | if base, ok := h.(DeleteInterface); ok { 239 | base.Delete(w, r) 240 | return 241 | } 242 | w.WriteHeader(http.StatusMethodNotAllowed) 243 | return 244 | } 245 | } 246 | 247 | // ThingsHandle things struct. 248 | type ThingsHandle struct { 249 | Things []*Thing 250 | basePath string 251 | } 252 | 253 | // Handle handle request. 254 | func (h *ThingsHandle) Handle(w http.ResponseWriter, r *http.Request) { 255 | if len(h.Things) == 1 { 256 | thingHandle := &ThingHandle{h.Things[0]} 257 | thingHandle.Handle(w, r) 258 | return 259 | } 260 | 261 | BaseHandle(h, w, r) 262 | } 263 | 264 | // Get Handle a Get request. 265 | // 266 | // @param {Object} r The request object 267 | // @param {Object} w The response object 268 | func (h *ThingsHandle) Get(w http.ResponseWriter, r *http.Request) { 269 | 270 | var things []json.RawMessage 271 | for _, thing := range h.Things { 272 | things = append(things, thing.AsThingDescription()) 273 | } 274 | content, _ := json.Marshal(things) 275 | 276 | if _, err := w.Write(content); err != nil { 277 | fmt.Println(err) 278 | } 279 | } 280 | 281 | // ThingHandle Handle a request to thing. 282 | type ThingHandle struct { 283 | *Thing 284 | } 285 | 286 | // Handle a request to /thing. 287 | func (h *ThingHandle) Handle(w http.ResponseWriter, r *http.Request) { 288 | BaseHandle(h, w, r) 289 | } 290 | 291 | // Get Handle a Get request. 292 | // 293 | // @param {Object} r The request object 294 | // @param {Object} w The response object 295 | func (h *ThingHandle) Get(w http.ResponseWriter, r *http.Request) { 296 | base := h.Thing.AsThingDescription() 297 | 298 | var ls map[string][]Link 299 | json.Unmarshal(base, &ls) 300 | 301 | scheme := "ws" 302 | // if r.URL.Scheme != "" { 303 | // scheme = r.URL.Scheme 304 | // } 305 | wsHref := fmt.Sprintf("%s://%s%s", scheme, r.Host, h.Href()) 306 | ls["links"] = append(ls["links"], Link{ 307 | Rel: "alternate", 308 | Href: filepath.Clean(strings.TrimRight(wsHref+"/"+h.Href(), "/")), 309 | }) 310 | var desc map[string]interface{} 311 | if err := json.Unmarshal(base, &desc); err != nil { 312 | fmt.Print(err) 313 | } 314 | desc["links"] = ls["links"] 315 | 316 | type securityDefinitions struct { 317 | NosecSc struct { 318 | Scheme string `json:"scheme"` 319 | } `json:"nosec_sc"` 320 | } 321 | sec := &securityDefinitions{} 322 | sec.NosecSc.Scheme = "nosec" 323 | desc["securityDefinitions"] = sec 324 | desc["security"] = "nosec_sc" 325 | 326 | re, _ := json.Marshal(desc) 327 | if _, err := w.Write(re); err != nil { 328 | fmt.Println(err) 329 | } 330 | } 331 | 332 | // PropertiesHandle Handle a request to /properties. 333 | type PropertiesHandle struct { 334 | *ThingHandle 335 | } 336 | 337 | // Handle Handle a request to /properties. 338 | func (h *PropertiesHandle) Handle(w http.ResponseWriter, r *http.Request) { 339 | if name, err := resource(trimSlash(r.RequestURI)); err == nil { 340 | propertyHandle := &PropertyHandle{h, h.properties[name]} 341 | propertyHandle.Handle(w, r) 342 | return 343 | } 344 | 345 | BaseHandle(h, w, r) 346 | } 347 | 348 | // Get Handle a Get request. 349 | // 350 | // @param {Object} r The request object 351 | // @param {Object} w The response object 352 | func (h *PropertiesHandle) Get(w http.ResponseWriter, r *http.Request) { 353 | content, err := json.Marshal(h.Thing.Properties()) 354 | if err != nil { 355 | fmt.Println(err) 356 | } 357 | if _, err := w.Write(content); err != nil { 358 | fmt.Println(err) 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /thing.go: -------------------------------------------------------------------------------- 1 | package webthing 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/gorilla/websocket" 11 | "github.com/xeipuuv/gojsonschema" 12 | ) 13 | 14 | // Thing A Web Thing struct. 15 | type Thing struct { 16 | id string 17 | context string 18 | atType []string 19 | title string 20 | description string 21 | properties map[string]*Property 22 | availableActions map[string]*AvailableAction 23 | availableEvents map[string]*AvailableEvent 24 | actions map[string][]*Action 25 | events []*Event 26 | subscribers map[string]*websocket.Conn 27 | hrefPrefix string 28 | uiHref string 29 | } 30 | 31 | // ThingMember thingmember 32 | type ThingMember struct { 33 | ID string `json:"id"` 34 | Context string `json:"@context"` 35 | AtType []string `json:"@type"` 36 | Title string `json:"title"` 37 | Description string `json:"description,omitempty"` 38 | Properties json.RawMessage `json:"properties,omitempty"` 39 | Actions map[string]json.RawMessage `json:"actions,omitempty"` 40 | Events map[string]json.RawMessage `json:"events,omitempty"` 41 | Links []Link `json:"links"` 42 | } 43 | 44 | func NewThingMember(thing *Thing) *ThingMember { 45 | th := &ThingMember{ 46 | ID: thing.id, 47 | Title: thing.title, 48 | Context: thing.context, 49 | AtType: thing.atType, 50 | Description: thing.description, 51 | Properties: json.RawMessage{}, 52 | Actions: make(map[string]json.RawMessage), 53 | Events: make(map[string]json.RawMessage), 54 | } 55 | return th 56 | } 57 | 58 | // NewThing create a thing. 59 | func NewThing(id, title string, atType []string, description string) *Thing { 60 | thing := &Thing{} 61 | thing.id = id 62 | thing.title = title 63 | thing.context = "https://webthings.io/schemas" 64 | thing.atType = atType 65 | thing.description = description 66 | thing.properties = make(map[string]*Property) 67 | thing.availableActions = make(map[string]*AvailableAction) 68 | thing.availableEvents = make(map[string]*AvailableEvent) 69 | thing.actions = make(map[string][]*Action) 70 | thing.events = []*Event{} 71 | thing.subscribers = map[string]*websocket.Conn{} 72 | thing.hrefPrefix = "" 73 | thing.uiHref = "" 74 | return thing 75 | } 76 | 77 | // Link base link struct 78 | type Link struct { 79 | Href string `json:"href,omitempty"` 80 | Rel string `json:"rel,omitempty"` 81 | MediaType string `json:"mediaType,omitempty"` 82 | } 83 | 84 | func (th *ThingMember) availableActionsDesc(thing *Thing) { 85 | for name := range thing.availableActions { 86 | meta := thing.availableActions[name].Metadata() 87 | var m map[string]interface{} 88 | json.Unmarshal(meta, &m) 89 | m["links"] = []Link{{ 90 | Rel: "action", 91 | Href: filepath.Clean(fmt.Sprintf("/%s/actions/%s", thing.Href(), name)), 92 | }} 93 | obj, _ := json.Marshal(m) 94 | th.Actions[name] = obj 95 | } 96 | } 97 | 98 | func (th *ThingMember) availableEventsDesc(thing *Thing) { 99 | for name := range thing.availableEvents { 100 | meta, _ := thing.availableEvents[name].Metadata().MarshalJSON() 101 | var m map[string]interface{} 102 | json.Unmarshal(meta, &m) 103 | m["links"] = []Link{{ 104 | Rel: "events", 105 | Href: filepath.Clean(fmt.Sprintf("/%s/events/%s", thing.Href(), name)), 106 | }} 107 | obj, _ := json.Marshal(m) 108 | th.Events[name] = obj 109 | } 110 | } 111 | 112 | func (th *ThingMember) links(thing *Thing) { 113 | for _, name := range []string{"properties", "actions", "events"} { 114 | th.Links = append(th.Links, Link{ 115 | Rel: name, 116 | Href: fmt.Sprintf("%s/%s", thing.hrefPrefix, name), 117 | }) 118 | } 119 | 120 | if thing.UIHref() != "" { 121 | th.Links = append(th.Links, Link{ 122 | Rel: "alternate", 123 | MediaType: "text/html", 124 | Href: filepath.Clean(thing.UIHref()), 125 | }) 126 | } 127 | } 128 | 129 | // AsThingDescription retrun []byte data of thing struct. 130 | // Return the thing state as a Thing Description. 131 | // @returns {Object} Current thing state 132 | func (thing *Thing) AsThingDescription() []byte { 133 | 134 | th := NewThingMember(thing) 135 | 136 | th.Properties = []byte(thing.PropertyDescriptions()) 137 | th.availableActionsDesc(thing) 138 | th.availableEventsDesc(thing) 139 | th.links(thing) 140 | 141 | thingDescription, err := json.Marshal(th) 142 | if err != nil { 143 | fmt.Println(err.Error()) 144 | } 145 | 146 | return thingDescription 147 | } 148 | 149 | // Href Get this thing's href. 150 | // 151 | // @returns {String} The href. 152 | func (thing *Thing) Href() string { 153 | if thing.hrefPrefix != "" { 154 | return thing.hrefPrefix 155 | } 156 | 157 | return "/" 158 | } 159 | 160 | // UIHref Get this thing's UI href. 161 | // 162 | // @returns {String|null} The href. 163 | func (thing *Thing) UIHref() string { 164 | return thing.uiHref 165 | } 166 | 167 | // SetHrefPrefix Set the prefix of any hrefs associated with this thing. 168 | // 169 | // @param {String} prefix The prefix 170 | func (thing *Thing) SetHrefPrefix(prefix string) { 171 | thing.hrefPrefix = prefix 172 | for name := range thing.properties { 173 | thing.properties[name].SetHrefPrefix(prefix) 174 | } 175 | for name := range thing.actions { 176 | for key := range thing.actions[name] { 177 | thing.actions[name][key].SetHrefPrefix(prefix) 178 | } 179 | } 180 | } 181 | 182 | // SetUIHref Set the href of this thing's custom UI. 183 | // 184 | // @param {String} href The href 185 | func (thing *Thing) SetUIHref(href string) { 186 | thing.uiHref = href 187 | } 188 | 189 | // ID Get the ID of the thing. 190 | // 191 | // @returns {String} The ID. 192 | func (thing *Thing) ID() string { 193 | return thing.id 194 | } 195 | 196 | // Title Get the title of the thing. 197 | // 198 | // @returns {String} The title. 199 | func (thing *Thing) Title() string { 200 | return thing.title 201 | } 202 | 203 | // Context Get the type context of the thing. 204 | // 205 | // @returns {String} The contexthing. 206 | func (thing *Thing) Context() string { 207 | return thing.context 208 | } 209 | 210 | // Type Get the type(s) of the thing. 211 | // 212 | // @returns {String[]} The type(s). 213 | func (thing *Thing) Type() []string { 214 | return thing.atType 215 | } 216 | 217 | // Description Get the description of the thing. 218 | // 219 | // @returns {String} The description. 220 | func (thing *Thing) Description() string { 221 | return thing.description 222 | } 223 | 224 | // PropertyDescriptions Get the thing's properties as an object. 225 | // 226 | // @returns {Object} Properties, i.e. name -> description 227 | func (thing *Thing) PropertyDescriptions() string { 228 | descriptions := make(map[string]json.RawMessage) 229 | for name, property := range thing.properties { 230 | descriptions[name] = []byte(property.AsPropertyDescription()) 231 | } 232 | 233 | str, _ := json.Marshal(descriptions) 234 | return string(str) 235 | } 236 | 237 | // ActionDescriptions Get the thing's actions as an array. 238 | // 239 | // @param {String?} actionName Optional action name to get descriptions for 240 | // @returns {Object} Action descriptions. 241 | func (thing *Thing) ActionDescriptions(actionName string) (descriptions []json.RawMessage) { 242 | if actionName != "" { 243 | return actionsDescription(thing, descriptions, actionName) 244 | } 245 | 246 | for name := range thing.actions { 247 | descriptions = actionsDescription(thing, descriptions, name) 248 | } 249 | 250 | return descriptions 251 | } 252 | 253 | func actionsDescription(thing *Thing, descriptions []json.RawMessage, actionName string) []json.RawMessage { 254 | if actions, ok := thing.actions[actionName]; ok { 255 | for _, action := range actions { 256 | if action != nil { 257 | descriptions = append(descriptions, []byte(action.AsActionDescription())) 258 | } 259 | } 260 | } 261 | return descriptions 262 | } 263 | 264 | // EventDescriptions Get the thing's events as an array. 265 | // 266 | //@param {String?} eventName Optional event name to get descriptions for 267 | // 268 | //@returns {Object} Event descriptions. 269 | func (thing *Thing) EventDescriptions(eventName string) []byte { 270 | var descriptions []json.RawMessage 271 | if len(thing.events) == 0 { 272 | return []byte(`{}`) 273 | } 274 | for _, event := range thing.events { 275 | if eventName == "" || strings.EqualFold(event.Name(), eventName) { 276 | descriptions = append(descriptions, event.AsEventDescription()) 277 | } 278 | } 279 | 280 | content, _ := json.Marshal(descriptions) 281 | return content 282 | } 283 | 284 | // AddProperty Add a property to this thing. 285 | // 286 | // @param property Property to add. 287 | func (thing *Thing) AddProperty(property *Property) { 288 | property.SetHrefPrefix(thing.hrefPrefix) 289 | thing.properties[property.Name()] = property 290 | } 291 | 292 | // RemoveProperty Remove a property from this thing. 293 | // 294 | // @param property Property to remove. 295 | func (thing *Thing) RemoveProperty(property Property) { 296 | if p, ok := thing.properties[property.Name()]; ok { 297 | delete(thing.properties, p.Name()) 298 | } 299 | } 300 | 301 | // Find a property by name. 302 | // 303 | // @param propertyName Name of the property to find 304 | // @return Property if found, else null. 305 | func (thing *Thing) findProperty(propertyName string) (*Property, bool) { 306 | if p, ok := thing.properties[propertyName]; ok { 307 | return p, true 308 | } 309 | return &Property{}, false 310 | } 311 | 312 | // Property Get a property's value. 313 | // 314 | // @param propertyName Name of the property to get the value of 315 | // @param Type of the property value 316 | // @return Current property value if found, else null. 317 | func (thing *Thing) Property(propertyName string) *Value { 318 | if prop, ok := thing.findProperty(propertyName); ok { 319 | return prop.Value() 320 | } 321 | return &Value{} 322 | } 323 | 324 | // Properties et a mapping of all properties and their values. 325 | // 326 | // @return JSON object of propertyName -> value. 327 | func (thing *Thing) Properties() map[string]interface{} { 328 | properties := make(map[string]interface{}) 329 | for name, property := range thing.properties { 330 | properties[name] = property.Value().Get() 331 | } 332 | return properties 333 | } 334 | 335 | // Determine whether or not this thing has a given property. 336 | // 337 | // @param propertyName The property to look for 338 | // @return Indication of property presence. 339 | func (thing *Thing) HasProperty(propertyName string) bool { 340 | if _, ok := thing.properties[propertyName]; ok { 341 | return true 342 | } 343 | return false 344 | } 345 | 346 | // SetProperty Set a property value. 347 | // 348 | // @param propertyName Name of the property to set 349 | // @param value Value to set 350 | // @param Type of the property value 351 | // @throws PropertyError If value could not be set. 352 | func (thing *Thing) SetProperty(propertyName string, value *Value) error { 353 | if _, ok := thing.findProperty(propertyName); !ok { 354 | return errors.New(`"General property error"`) 355 | } 356 | property := thing.properties[propertyName] 357 | return property.SetValue(value) 358 | } 359 | 360 | // Action Get an action. 361 | // 362 | // @param actionName Name of the action 363 | // @param actionId ID of the action 364 | // @return The requested action if found, else null. 365 | func (thing *Thing) Action(actionName, actionID string) (action *Action) { 366 | if _, ok := thing.actions[actionName]; !ok { 367 | return nil 368 | } 369 | for _, ac := range thing.actions[actionName] { 370 | // Each newly created action must contain a new uuid, 371 | // otherwise a random action with the same uuid will be found and returned. 372 | if ac != nil && ac.ID() == actionID { 373 | action = ac 374 | } 375 | } 376 | return action 377 | } 378 | 379 | // AddEvent Add a new event and notify subscribers. 380 | // 381 | // @param event The event that occurred. 382 | func (thing *Thing) AddEvent(event *Event) { 383 | thing.events = append(thing.events, event) 384 | thing.EventNotify(event) 385 | } 386 | 387 | // AddAvailableEvent Add an available event. 388 | // 389 | // @param name Name of the event 390 | // @param metadata Event metadata, i.e. type, description, etc., as a 391 | // JSONObject 392 | func (thing *Thing) AddAvailableEvent(name string, metadata json.RawMessage) { 393 | thing.availableEvents[name] = NewAvailableEvent(metadata) 394 | } 395 | 396 | // PerformAction Perform an action on the thing. 397 | // 398 | // @param actionName Name of the action 399 | // @param input Any action inputs 400 | // @return The action that was created. 401 | func (thing *Thing) PerformAction(actionName string, input *json.RawMessage) (*Action, error) { 402 | if _, ok := thing.availableActions[actionName]; !ok { 403 | fmt.Print("Not found action: ", actionName) 404 | return nil, errors.New("Not found action: " + actionName) 405 | } 406 | 407 | actionType := thing.availableActions[actionName] 408 | 409 | schemaLoader := gojsonschema.NewGoLoader(actionType.schema) 410 | documentLoader := gojsonschema.NewGoLoader(input) 411 | result, err := gojsonschema.Validate(schemaLoader, documentLoader) 412 | 413 | if err != nil { 414 | str1, _ := schemaLoader.LoadJSON() 415 | fmt.Println(str1) 416 | str2, _ := documentLoader.LoadJSON() 417 | fmt.Println(str2) 418 | fmt.Println(err) 419 | return nil, err 420 | } 421 | // if result.Valid() { 422 | // fmt.Printf("The document is valid\n") 423 | // } 424 | if !result.Valid() { 425 | fmt.Printf("The document is not valid. see errors :\n") 426 | for _, desc := range result.Errors() { 427 | fmt.Printf("- %s\n", desc) 428 | } 429 | return nil, err 430 | } 431 | // if !actionType.ValidateActionInput(input) { 432 | // return nil 433 | // } 434 | 435 | cls := actionType.getCls() 436 | 437 | // The Generator is called to create an action. 438 | action := cls.Generator(thing) 439 | action.SetInput(input) 440 | action.SetHrefPrefix(thing.hrefPrefix) 441 | 442 | thing.ActionNotify(action) 443 | thing.actions[actionName] = append(thing.actions[actionName], action) 444 | 445 | return action, nil 446 | } 447 | 448 | // RemoveAction Remove an existing action. 449 | // 450 | // @param actionName name of the action 451 | // @param actionId ID of the action 452 | // @return Boolean indicating the presence of the action. 453 | func (thing *Thing) RemoveAction(actionName, actionID string) bool { 454 | action := thing.Action(actionName, actionID) 455 | if action.ID() == "" { 456 | return false 457 | } 458 | 459 | defer action.Cancel() // Cancel action after delete from origin. 460 | 461 | actions := thing.actions[actionName] 462 | for k, ac := range actions { 463 | if ac != nil && ac.ID() == actionID { 464 | actions[k] = nil 465 | } 466 | } 467 | 468 | return true 469 | } 470 | 471 | // AddAvailableAction Add an available action. 472 | // 473 | // @param name Name of the action 474 | // @param metadata Action metadata, i.e. type, description, etc., as a 475 | // JSONObject 476 | // @param action Instantiate for this action 477 | func (thing *Thing) AddAvailableAction(name string, metadata json.RawMessage, action Actioner) { 478 | thing.availableActions[name] = NewAvailableAction(metadata, action) 479 | thing.actions[name] = []*Action{} 480 | } 481 | 482 | // AddSubscriber Add a new websocket subscriber. 483 | // 484 | // @param ws The websocket 485 | func (thing *Thing) AddSubscriber(wsID string, ws *websocket.Conn) { 486 | thing.subscribers[wsID] = ws 487 | } 488 | 489 | // RemoveSubscriber Remove a websocket subscriber. 490 | // 491 | // @param ws The websocket 492 | func (thing *Thing) RemoveSubscriber(name string, ws *websocket.Conn) { 493 | 494 | delete(thing.subscribers, name) 495 | 496 | for name := range thing.availableEvents { 497 | thing.RemoveEventSubscriber(name, ws) 498 | } 499 | } 500 | 501 | // AddEventSubscriber Add a new websocket subscriber to an event. 502 | // 503 | // @param name Name of the event 504 | // @param ws The websocket 505 | func (thing *Thing) AddEventSubscriber() {} 506 | 507 | // RemoveEventSubscriber Remove a websocket subscriber from an event. 508 | // 509 | // @param name Name of the event 510 | // @param ws The websocket 511 | func (thing *Thing) RemoveEventSubscriber(name string, ws *websocket.Conn) error { 512 | 513 | delete(thing.availableEvents, name) 514 | 515 | if _, ok := thing.availableEvents[name]; ok { 516 | for _, eventWS := range thing.availableEvents[name].subscribers { 517 | if err := eventWS.Close(); err != nil { 518 | return err 519 | } 520 | } 521 | } 522 | return nil 523 | } 524 | 525 | type message struct { 526 | MessageType string `json:"messageType"` 527 | Data json.RawMessage `json:"data"` 528 | } 529 | 530 | // PropertyNotify Notify all subscribers of a property change. 531 | // 532 | // @param property The property that changed 533 | func (thing *Thing) PropertyNotify(property Property) error { 534 | str := message{ 535 | MessageType: "propertyStatus", 536 | Data: property.AsPropertyDescription(), 537 | } 538 | msg, err := json.Marshal(str) 539 | if err != nil { 540 | return err 541 | } 542 | for _, sub := range thing.subscribers { 543 | if err := sub.WriteJSON(msg); err != nil { 544 | return err 545 | } 546 | } 547 | return nil 548 | } 549 | 550 | // ActionNotify Notify all subscribers of an action status change. 551 | // 552 | // @param action The action whose status changed 553 | func (thing *Thing) ActionNotify(action *Action) error { 554 | str := message{ 555 | MessageType: "actionStatus", 556 | Data: action.AsActionDescription(), 557 | } 558 | msg, err := json.Marshal(str) 559 | if err != nil { 560 | return err 561 | } 562 | for _, sub := range thing.subscribers { 563 | if err := sub.WriteJSON(msg); err != nil { 564 | return err 565 | } 566 | } 567 | return nil 568 | } 569 | 570 | // EventNotify Notify all subscribers of an event. 571 | // 572 | // @param event The event that occurred 573 | func (thing *Thing) EventNotify(event *Event) error { 574 | eventName := event.Name() 575 | if _, ok := thing.availableEvents[eventName]; !ok { 576 | return errors.New("Event not found. ") 577 | } 578 | str := message{ 579 | MessageType: "event", 580 | Data: event.AsEventDescription(), 581 | } 582 | msg, err := json.Marshal(str) 583 | if err != nil { 584 | return err 585 | } 586 | for _, sub := range thing.subscribers { 587 | if err := sub.WriteJSON(msg); err != nil { 588 | return err 589 | } 590 | } 591 | return nil 592 | } 593 | 594 | // AvailableEvent Class to describe an event available for subscription. 595 | type AvailableEvent struct { 596 | metadata json.RawMessage 597 | subscribers map[string]*websocket.Conn 598 | } 599 | 600 | // NewAvailableEvent Initialize the object. 601 | // 602 | // @param metadata The event metadata 603 | func NewAvailableEvent(metadata json.RawMessage) *AvailableEvent { 604 | return &AvailableEvent{metadata: metadata, subscribers: make(map[string]*websocket.Conn)} 605 | } 606 | 607 | // Metadata Get the event metadata. 608 | // 609 | // @return The metadata. 610 | func (ae *AvailableEvent) Metadata() json.RawMessage { 611 | return ae.metadata 612 | } 613 | 614 | // AvailableAction Class to describe an action available to be taken. 615 | type AvailableAction struct { 616 | metadata json.RawMessage 617 | action *Action 618 | schema interface{} 619 | cls Actioner 620 | } 621 | 622 | // NewAvailableAction Initialize the object. 623 | // 624 | // @param metadata The action metadata 625 | // @param action Instance for the action 626 | func NewAvailableAction(metadata json.RawMessage, cls Actioner) *AvailableAction { 627 | ac := &AvailableAction{} 628 | ac.metadata = metadata 629 | ac.cls = cls 630 | 631 | // Creating the map for input 632 | m := map[string]json.RawMessage{} 633 | json.Unmarshal(metadata, &m) 634 | if _, ok := m["input"]; ok { 635 | ac.schema = m["input"] 636 | } 637 | 638 | return ac 639 | } 640 | 641 | // Get the class to instantiate for the action. 642 | // 643 | // @return The class. 644 | func (ac *AvailableAction) getCls() Actioner { 645 | return ac.cls 646 | } 647 | 648 | // Metadata Get the action metadata. 649 | // 650 | // @return The metadata. 651 | func (ac *AvailableAction) Metadata() []byte { 652 | metaData, _ := json.Marshal(ac.metadata) 653 | return metaData 654 | } 655 | 656 | // Action Get the class to instantiate for the action. 657 | // 658 | // @return The class. 659 | func (ac *AvailableAction) Action() *Action { 660 | return ac.action 661 | } 662 | 663 | // ValidateActionInput Validate the input for a new action. 664 | // 665 | // @param actionInput The input to validate 666 | // @return Boolean indicating validation success. 667 | func (ac *AvailableAction) ValidateActionInput(actionInput interface{}) bool { 668 | if ac.schema == nil { 669 | return true 670 | } 671 | _, err := json.Marshal(actionInput) 672 | 673 | return err == nil 674 | } 675 | --------------------------------------------------------------------------------