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 (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"encoding/json/v2"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
|
|
@ -78,9 +78,7 @@ func ParseConfiguration() (conf SpaceapidConfig) {
|
|||
}
|
||||
|
||||
// Parse JSON
|
||||
dec := json.NewDecoder(bytes.NewReader(file))
|
||||
dec.DisallowUnknownFields()
|
||||
err = dec.Decode(&conf)
|
||||
err = json.UnmarshalRead(bytes.NewReader(file), &conf, json.RejectUnknownMembers(true))
|
||||
if err != nil {
|
||||
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
|
||||
|
||||
go 1.22
|
||||
go 1.27
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/json/v2"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
|
|
@ -19,7 +19,7 @@ func Root(resp *types.SpaceAPIResponse) func(http.ResponseWriter, *http.Request)
|
|||
}
|
||||
|
||||
// Serialize response
|
||||
response, err := json.Marshal(resp)
|
||||
response, err := json.Marshal(resp, json.Deterministic(true))
|
||||
if err != nil {
|
||||
log.Println("Failed to serialize JSON response:", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import (
|
|||
"git.hamburg.ccc.de/ccchh/spaceapid/types"
|
||||
)
|
||||
|
||||
func StateOpen(
|
||||
func StateOpenPUT(
|
||||
authDB config.HTTPBACredentials, validCredentials []config.HTTPBACredentialID,
|
||||
resp *types.SpaceAPIState,
|
||||
) func(http.ResponseWriter, *http.Request) {
|
||||
|
|
@ -22,7 +22,7 @@ func StateOpen(
|
|||
}
|
||||
|
||||
// Parse request body
|
||||
newState, err := strconv.ParseBool(string(body))
|
||||
doorNowOpen, err := strconv.ParseBool(string(body))
|
||||
if err != nil {
|
||||
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)
|
||||
|
|
@ -30,7 +30,28 @@ func StateOpen(
|
|||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
5
main.go
5
main.go
|
|
@ -59,7 +59,10 @@ func main() {
|
|||
)
|
||||
// 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
|
||||
http.HandleFunc("PUT /state/message",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
package persistence
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/json/jsontext"
|
||||
"encoding/json/v2"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
|
|
@ -20,7 +21,7 @@ func MergeOldState(response *types.SpaceAPIResponse) {
|
|||
var (
|
||||
err error
|
||||
oldState []byte
|
||||
persistedState types.PersistentSpaceState
|
||||
persistedState types.PersistentState
|
||||
)
|
||||
|
||||
log.Println("Merging old state from", persistentStatePath, "...")
|
||||
|
|
@ -87,27 +88,38 @@ func SaveCurrentState(response types.SpaceAPIResponse) {
|
|||
defer file.Close()
|
||||
|
||||
// Create persistent state
|
||||
persistentStateV14 := types.PersistentSpaceState{
|
||||
State: struct {
|
||||
Open bool `json:"open"`
|
||||
LastChange int64 `json:"lastchange"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}{Open: response.State.Open, LastChange: response.State.LastChange, Message: response.State.Message},
|
||||
var (
|
||||
isOpen bool
|
||||
isUnknown bool
|
||||
)
|
||||
switch response.State.Open {
|
||||
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
|
||||
persistentStateV14.Sensors = make(map[string][]types.PersistentEnvironmentSensor)
|
||||
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 {
|
||||
persistentStateV14.Sensors[key][i].Value = sensor.Value
|
||||
persistentStateV14.Sensors[key][i].Location = sensor.Location
|
||||
persistentStateV14.Sensors[key][i].Name = sensor.Name
|
||||
persistentStateV14.Sensors[key][i].LastChange = sensor.LastChange
|
||||
persistentState.Sensors[key][i].Value = sensor.Value
|
||||
persistentState.Sensors[key][i].Location = sensor.Location
|
||||
persistentState.Sensors[key][i].Name = sensor.Name
|
||||
persistentState.Sensors[key][i].LastChange = sensor.LastChange
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
log.Fatalln("Failed serializing persistent state... error:", err)
|
||||
}
|
||||
|
|
|
|||
62
types/v15.go
62
types/v15.go
|
|
@ -1,5 +1,9 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"encoding/json/v2"
|
||||
)
|
||||
|
||||
const CurrentMessageVersion = "15"
|
||||
|
||||
// SpaceAPIResponse represents the contents of the SpaceAPI message
|
||||
|
|
@ -114,19 +118,50 @@ type SpaceAPILocationAreas struct {
|
|||
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 {
|
||||
Open bool `json:"open,omitempty"`
|
||||
LastChange int64 `json:"lastchange,omitempty"`
|
||||
TriggerPerson string `json:"trigger_person,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Open SpaceAPIStateOpen `json:"open,omitzero"`
|
||||
LastChange int64 `json:"lastchange,omitempty"`
|
||||
TriggerPerson string `json:"trigger_person,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Icon struct {
|
||||
Open string `json:"open,omitempty"` // REQUIRED
|
||||
Closed string `json:"closed,omitempty"` // REQUIRED
|
||||
} `json:"icon,omitzero"`
|
||||
}
|
||||
|
||||
func (s *SpaceAPIState) FromPersistentSpaceState(ps PersistentSpaceState) {
|
||||
s.Open = ps.State.Open
|
||||
func (s *SpaceAPIState) FromPersistentSpaceState(ps PersistentState) {
|
||||
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.Message = ps.State.Message
|
||||
}
|
||||
|
|
@ -147,15 +182,18 @@ type EnvironmentSensor struct {
|
|||
LastChange int64 `json:"lastchange,omitempty"`
|
||||
}
|
||||
|
||||
type PersistentSpaceState struct {
|
||||
State struct {
|
||||
Open bool `json:"open"`
|
||||
LastChange int64 `json:"lastchange"`
|
||||
Message string `json:"message,omitempty"`
|
||||
} `json:"state"`
|
||||
type PersistentState struct {
|
||||
State PersistentSpaceState `json:"state"`
|
||||
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 {
|
||||
Value float64 `json:"value"`
|
||||
Location string `json:"location,omitempty"`
|
||||
|
|
|
|||
Loading…
Reference in a new issue