feat: Allow setting state->open to unknown via DELETE
The schema defines the absence of state->open to represent the unknown state. Previously it was not possible to explicitly set this state via the HTTP endpoints. types.SpaceAPIState.Open is now an enum which is either unknown, closed, or open. Custom IsZero() and MarshalJSON() methods on the type ensure marshaling the value yields a boolean for open/closed and an empty byte slice for unknown (resulting in the field being absent from the final response). The handlers.StateOpenDELETE() function has been added to expose this behavior. Switching to encoding/json/v2 required raising the minimum Go version in go.mod to 1.27. Closes: CCCHH/spaceapid!37 Fixes: CCCHH/spaceapid#38
This commit is contained in:
parent
a53a5c3517
commit
11046cb501
7 changed files with 110 additions and 38 deletions
|
|
@ -2,7 +2,7 @@ package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json/v2"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
@ -78,9 +78,7 @@ func ParseConfiguration() (conf SpaceapidConfig) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse JSON
|
// Parse JSON
|
||||||
dec := json.NewDecoder(bytes.NewReader(file))
|
err = json.UnmarshalRead(bytes.NewReader(file), &conf, json.RejectUnknownMembers(true))
|
||||||
dec.DisallowUnknownFields()
|
|
||||||
err = dec.Decode(&conf)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("Could not parse spaceapid config file:", err)
|
log.Fatalln("Could not parse spaceapid config file:", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
go.mod
2
go.mod
|
|
@ -1,3 +1,3 @@
|
||||||
module git.hamburg.ccc.de/ccchh/spaceapid
|
module git.hamburg.ccc.de/ccchh/spaceapid
|
||||||
|
|
||||||
go 1.22
|
go 1.27
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json/v2"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
|
@ -19,7 +19,7 @@ func Root(resp *types.SpaceAPIResponse) func(http.ResponseWriter, *http.Request)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serialize response
|
// Serialize response
|
||||||
response, err := json.Marshal(resp)
|
response, err := json.Marshal(resp, json.Deterministic(true))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Failed to serialize JSON response:", err)
|
log.Println("Failed to serialize JSON response:", err)
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import (
|
||||||
"git.hamburg.ccc.de/ccchh/spaceapid/types"
|
"git.hamburg.ccc.de/ccchh/spaceapid/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
func StateOpen(
|
func StateOpenPUT(
|
||||||
authDB config.HTTPBACredentials, validCredentials []config.HTTPBACredentialID,
|
authDB config.HTTPBACredentials, validCredentials []config.HTTPBACredentialID,
|
||||||
resp *types.SpaceAPIState,
|
resp *types.SpaceAPIState,
|
||||||
) func(http.ResponseWriter, *http.Request) {
|
) func(http.ResponseWriter, *http.Request) {
|
||||||
|
|
@ -22,7 +22,7 @@ func StateOpen(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse request body
|
// Parse request body
|
||||||
newState, err := strconv.ParseBool(string(body))
|
doorNowOpen, err := strconv.ParseBool(string(body))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Failed to parse request body from", r.RemoteAddr, "with error:", err)
|
log.Println("Failed to parse request body from", r.RemoteAddr, "with error:", err)
|
||||||
http.Error(w, "HTTP request body should either be true or false", http.StatusBadRequest)
|
http.Error(w, "HTTP request body should either be true or false", http.StatusBadRequest)
|
||||||
|
|
@ -30,7 +30,28 @@ func StateOpen(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set SpaceAPI response values
|
// Set SpaceAPI response values
|
||||||
resp.Open = newState
|
if doorNowOpen {
|
||||||
|
resp.Open = types.SpaceAPIStateOpenIsOpen
|
||||||
|
} else {
|
||||||
|
resp.Open = types.SpaceAPIStateOpenIsClosed
|
||||||
|
}
|
||||||
|
resp.LastChange = time.Now().Unix()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func StateOpenDELETE(
|
||||||
|
authDB config.HTTPBACredentials, validCredentials []config.HTTPBACredentialID,
|
||||||
|
resp *types.SpaceAPIState,
|
||||||
|
) func(http.ResponseWriter, *http.Request) {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
_, err := updateEndpointValidator(authDB, validCredentials, w, r)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set SpaceAPI response values
|
||||||
|
resp.Open = types.SpaceAPIStateOpenIsUnknown
|
||||||
resp.LastChange = time.Now().Unix()
|
resp.LastChange = time.Now().Unix()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
5
main.go
5
main.go
|
|
@ -59,7 +59,10 @@ func main() {
|
||||||
)
|
)
|
||||||
// state->open
|
// state->open
|
||||||
http.HandleFunc("PUT /state/open",
|
http.HandleFunc("PUT /state/open",
|
||||||
handlers.StateOpen(conf.Credentials, conf.Dynamic.State.Open.AllowedCredentials, &conf.Response.State),
|
handlers.StateOpenPUT(conf.Credentials, conf.Dynamic.State.Open.AllowedCredentials, &conf.Response.State),
|
||||||
|
)
|
||||||
|
http.HandleFunc("DELETE /state/open",
|
||||||
|
handlers.StateOpenDELETE(conf.Credentials, conf.Dynamic.State.Open.AllowedCredentials, &conf.Response.State),
|
||||||
)
|
)
|
||||||
// state->message
|
// state->message
|
||||||
http.HandleFunc("PUT /state/message",
|
http.HandleFunc("PUT /state/message",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
package persistence
|
package persistence
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json/jsontext"
|
||||||
|
"encoding/json/v2"
|
||||||
"errors"
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -20,7 +21,7 @@ func MergeOldState(response *types.SpaceAPIResponse) {
|
||||||
var (
|
var (
|
||||||
err error
|
err error
|
||||||
oldState []byte
|
oldState []byte
|
||||||
persistedState types.PersistentSpaceState
|
persistedState types.PersistentState
|
||||||
)
|
)
|
||||||
|
|
||||||
log.Println("Merging old state from", persistentStatePath, "...")
|
log.Println("Merging old state from", persistentStatePath, "...")
|
||||||
|
|
@ -87,27 +88,38 @@ func SaveCurrentState(response types.SpaceAPIResponse) {
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
// Create persistent state
|
// Create persistent state
|
||||||
persistentStateV14 := types.PersistentSpaceState{
|
var (
|
||||||
State: struct {
|
isOpen bool
|
||||||
Open bool `json:"open"`
|
isUnknown bool
|
||||||
LastChange int64 `json:"lastchange"`
|
)
|
||||||
Message string `json:"message,omitempty"`
|
switch response.State.Open {
|
||||||
}{Open: response.State.Open, LastChange: response.State.LastChange, Message: response.State.Message},
|
case types.SpaceAPIStateOpenIsUnknown:
|
||||||
|
isUnknown = true
|
||||||
|
case types.SpaceAPIStateOpenIsClosed:
|
||||||
|
break
|
||||||
|
case types.SpaceAPIStateOpenIsOpen:
|
||||||
|
isOpen = true
|
||||||
|
}
|
||||||
|
persistentState := types.PersistentState{
|
||||||
|
State: types.PersistentSpaceState{
|
||||||
|
Open: isOpen, IsUnknown: isUnknown,
|
||||||
|
LastChange: response.State.LastChange, Message: response.State.Message,
|
||||||
|
},
|
||||||
|
Sensors: make(map[string][]types.PersistentEnvironmentSensor),
|
||||||
}
|
}
|
||||||
// Save sensor state
|
// Save sensor state
|
||||||
persistentStateV14.Sensors = make(map[string][]types.PersistentEnvironmentSensor)
|
|
||||||
for key, environmentSensors := range response.Sensors {
|
for key, environmentSensors := range response.Sensors {
|
||||||
persistentStateV14.Sensors[key] = make([]types.PersistentEnvironmentSensor, len(environmentSensors))
|
persistentState.Sensors[key] = make([]types.PersistentEnvironmentSensor, len(environmentSensors))
|
||||||
for i, sensor := range environmentSensors {
|
for i, sensor := range environmentSensors {
|
||||||
persistentStateV14.Sensors[key][i].Value = sensor.Value
|
persistentState.Sensors[key][i].Value = sensor.Value
|
||||||
persistentStateV14.Sensors[key][i].Location = sensor.Location
|
persistentState.Sensors[key][i].Location = sensor.Location
|
||||||
persistentStateV14.Sensors[key][i].Name = sensor.Name
|
persistentState.Sensors[key][i].Name = sensor.Name
|
||||||
persistentStateV14.Sensors[key][i].LastChange = sensor.LastChange
|
persistentState.Sensors[key][i].LastChange = sensor.LastChange
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serialize persistent state
|
// Serialize persistent state
|
||||||
marshal, err := json.MarshalIndent(persistentStateV14, "", "\t")
|
marshal, err := json.Marshal(persistentState, json.JoinOptions(jsontext.Multiline(true), jsontext.SpaceAfterComma(true)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("Failed serializing persistent state... error:", err)
|
log.Fatalln("Failed serializing persistent state... error:", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
62
types/v15.go
62
types/v15.go
|
|
@ -1,5 +1,9 @@
|
||||||
package types
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json/v2"
|
||||||
|
)
|
||||||
|
|
||||||
const CurrentMessageVersion = "15"
|
const CurrentMessageVersion = "15"
|
||||||
|
|
||||||
// SpaceAPIResponse represents the contents of the SpaceAPI message
|
// SpaceAPIResponse represents the contents of the SpaceAPI message
|
||||||
|
|
@ -114,19 +118,50 @@ type SpaceAPILocationAreas struct {
|
||||||
SquareMeters float64 `json:"square_meters"` // REQUIRED
|
SquareMeters float64 `json:"square_meters"` // REQUIRED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SpaceAPIStateOpen int
|
||||||
|
|
||||||
|
const (
|
||||||
|
SpaceAPIStateOpenIsUnknown = iota
|
||||||
|
SpaceAPIStateOpenIsClosed
|
||||||
|
SpaceAPIStateOpenIsOpen
|
||||||
|
)
|
||||||
|
|
||||||
|
func (state SpaceAPIStateOpen) MarshalJSON() ([]byte, error) {
|
||||||
|
switch state {
|
||||||
|
case SpaceAPIStateOpenIsClosed:
|
||||||
|
return json.Marshal(false)
|
||||||
|
case SpaceAPIStateOpenIsOpen:
|
||||||
|
return json.Marshal(true)
|
||||||
|
default:
|
||||||
|
return []byte{}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state SpaceAPIStateOpen) IsZero() bool {
|
||||||
|
return state == SpaceAPIStateOpenIsUnknown
|
||||||
|
}
|
||||||
|
|
||||||
type SpaceAPIState struct {
|
type SpaceAPIState struct {
|
||||||
Open bool `json:"open,omitempty"`
|
Open SpaceAPIStateOpen `json:"open,omitzero"`
|
||||||
LastChange int64 `json:"lastchange,omitempty"`
|
LastChange int64 `json:"lastchange,omitempty"`
|
||||||
TriggerPerson string `json:"trigger_person,omitempty"`
|
TriggerPerson string `json:"trigger_person,omitempty"`
|
||||||
Message string `json:"message,omitempty"`
|
Message string `json:"message,omitempty"`
|
||||||
Icon struct {
|
Icon struct {
|
||||||
Open string `json:"open,omitempty"` // REQUIRED
|
Open string `json:"open,omitempty"` // REQUIRED
|
||||||
Closed string `json:"closed,omitempty"` // REQUIRED
|
Closed string `json:"closed,omitempty"` // REQUIRED
|
||||||
} `json:"icon,omitzero"`
|
} `json:"icon,omitzero"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SpaceAPIState) FromPersistentSpaceState(ps PersistentSpaceState) {
|
func (s *SpaceAPIState) FromPersistentSpaceState(ps PersistentState) {
|
||||||
s.Open = ps.State.Open
|
if ps.State.IsUnknown {
|
||||||
|
s.Open = SpaceAPIStateOpenIsUnknown
|
||||||
|
} else {
|
||||||
|
if ps.State.Open {
|
||||||
|
s.Open = SpaceAPIStateOpenIsOpen
|
||||||
|
} else {
|
||||||
|
s.Open = SpaceAPIStateOpenIsClosed
|
||||||
|
}
|
||||||
|
}
|
||||||
s.LastChange = ps.State.LastChange
|
s.LastChange = ps.State.LastChange
|
||||||
s.Message = ps.State.Message
|
s.Message = ps.State.Message
|
||||||
}
|
}
|
||||||
|
|
@ -147,15 +182,18 @@ type EnvironmentSensor struct {
|
||||||
LastChange int64 `json:"lastchange,omitempty"`
|
LastChange int64 `json:"lastchange,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PersistentSpaceState struct {
|
type PersistentState struct {
|
||||||
State struct {
|
State PersistentSpaceState `json:"state"`
|
||||||
Open bool `json:"open"`
|
|
||||||
LastChange int64 `json:"lastchange"`
|
|
||||||
Message string `json:"message,omitempty"`
|
|
||||||
} `json:"state"`
|
|
||||||
Sensors map[string][]PersistentEnvironmentSensor `json:"sensors,omitempty"`
|
Sensors map[string][]PersistentEnvironmentSensor `json:"sensors,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PersistentSpaceState struct {
|
||||||
|
Open bool `json:"open"`
|
||||||
|
IsUnknown bool `json:"is_unknown"`
|
||||||
|
LastChange int64 `json:"lastchange"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type PersistentEnvironmentSensor struct {
|
type PersistentEnvironmentSensor struct {
|
||||||
Value float64 `json:"value"`
|
Value float64 `json:"value"`
|
||||||
Location string `json:"location,omitempty"`
|
Location string `json:"location,omitempty"`
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue