REST API: simplify json response by removing result wrapper (BC) (#22299)

This commit is contained in:
andig 2025-08-01 15:00:14 +02:00 • committed by GitHub
parent db824ad42d
commit d541dfa4fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 423 additions and 293 deletions

View file

@ -79,9 +79,6 @@ snapshot::
release::
goreleaser --clean
openapi::
cd server/mcp && go run github.com/evcc-io/evcc/server/mcp/openapi https://raw.githubusercontent.com/evcc-io/docs/refs/heads/main/static/rest-api.yaml
docker::
@echo Version: $(VERSION) $(SHA) $(BUILD_DATE)
docker buildx build --platform $(PLATFORM) --tag $(DOCKER_IMAGE):$(DOCKER_TAG) --push .

View file

@ -232,7 +232,7 @@ export default defineComponent({
async updateActivePlan(): Promise<void> {
try {
const res = await this.apiFetchPlan(`loadpoints/${this.id}/plan`);
this.plan = res?.data.result ?? ({} as PlanWrapper);
this.plan = res?.data ?? ({} as PlanWrapper);
this.nextPlanId = this.plan.planId;
} catch (e) {
console.error(e);
@ -271,7 +271,7 @@ export default defineComponent({
params,
})) as PlanResponse;
if (res.status === 404) {
return { data: { result: {} as PlanWrapper } } as PlanResponse;
return { data: {} } as PlanResponse;
}
return res;
} catch (e) {
@ -322,7 +322,7 @@ export default defineComponent({
precondition,
});
}
this.plan = planRes?.data.result ?? ({} as PlanWrapper);
this.plan = planRes?.data ?? ({} as PlanWrapper);
} catch (e) {
console.error(e);
}

View file

@ -19,7 +19,7 @@ export interface PlanWrapper {
export interface PlanResponse {
status: number;
data: { result: PlanWrapper };
data: PlanWrapper;
}
export type StaticPlan = StaticSocPlan | StaticEnergyPlan;

View file

@ -103,12 +103,12 @@ export function createDeviceUtils(deviceType: DeviceType) {
async function load(id: number) {
const response = await api.get(`config/devices/${deviceType}/${id}`);
return response.data.result;
return response.data;
}
async function create(data: any) {
const response = await api.post(`config/devices/${deviceType}`, data);
return response.data.result;
return response.data;
}
async function loadProducts(lang?: string, usage?: string) {
@ -117,7 +117,7 @@ export function createDeviceUtils(deviceType: DeviceType) {
params["usage"] = usage;
}
const response = await api.get(`config/products/${deviceType}`, { params });
return response.data.result;
return response.data;
}
async function loadTemplate(templateName: string, lang?: string) {
@ -130,7 +130,7 @@ export function createDeviceUtils(deviceType: DeviceType) {
},
};
const response = await api.get(`config/templates/${deviceType}`, opts);
return response.data.result;
return response.data;
}
return {

View file

@ -125,7 +125,7 @@ export default {
validateStatus: (code) => [200, 404].includes(code),
});
if (res.status === 200) {
this.title = res.data.result.title;
this.title = res.data.title;
} else {
console.log("TODO: implement site endpoint in config error mode");
}

View file

@ -795,7 +795,7 @@ export default {
async loadConfiguration() {
try {
const res = await api.get(`config/loadpoints/${this.id}`);
this.values = deepClone(res.data.result);
this.values = deepClone(res.data);
this.updateChargerPower();
this.updateSolarMode();
this.updatePhases();

View file

@ -100,8 +100,8 @@ export default {
async load() {
try {
const { data } = await api.get(this.endpoint);
this.serverYaml = data.result;
this.yaml = data.result || this.defaultYaml;
this.serverYaml = data;
this.yaml = data || this.defaultYaml;
} catch (e) {
console.error(e);
}

View file

@ -38,7 +38,7 @@ export default {
this.testState = TEST_RUNNING;
try {
const res = await testApi();
for (const [key, { error }] of Object.entries(res.data.result)) {
for (const [key, { error }] of Object.entries(res.data)) {
if (error) {
this.testState = TEST_FAILED;
this.testResult = null;
@ -46,7 +46,7 @@ export default {
return false;
}
}
this.testResult = res.data.result;
this.testResult = res.data;
this.testError = null;
this.testState = TEST_SUCCESS;
return true;

View file

@ -33,7 +33,7 @@ export const performTest = async (
state.errorLine = null;
try {
const res = await api();
for (const [key, value] of Object.entries(res.data.result)) {
for (const [key, value] of Object.entries(res.data)) {
const { error } = value as { error?: string };
if (error) {
state.isError = true;
@ -42,7 +42,7 @@ export const performTest = async (
}
}
state.isSuccess = true;
state.result = res.data.result;
state.result = res.data;
return true;
} catch (e: any) {
state.isError = true;

View file

@ -331,7 +331,7 @@ export default defineComponent({
async updateReferenceGrid() {
try {
const res = await api.get(`tariff/grid`, allowClientError);
const { rates } = res.data.result as { rates: Rate[] };
const { rates } = res.data as { rates: Rate[] };
this.referenceGrid =
rates.reduce((acc, slot) => {
return acc + slot.value;

View file

@ -646,37 +646,37 @@ export default {
},
async loadDirty() {
const response = await api.get("/config/dirty");
if (response.data?.result) {
if (response.data) {
restart.restartNeeded = true;
}
},
async loadVehicles() {
const response = await api.get("/config/devices/vehicle");
this.vehicles = response.data?.result || [];
this.vehicles = response.data || [];
},
async loadChargers() {
const response = await api.get("/config/devices/charger");
this.chargers = response.data?.result || [];
this.chargers = response.data || [];
},
async loadMeters() {
const response = await api.get("/config/devices/meter");
this.meters = response.data?.result || [];
this.meters = response.data || [];
},
async loadCircuits() {
const response = await api.get("/config/devices/circuit");
this.circuits = response.data?.result || [];
this.circuits = response.data || [];
},
async loadSite() {
const response = await api.get("/config/site", {
validateStatus: (status) => status < 500,
});
if (response.status === 200) {
this.site = response.data?.result;
this.site = response.data;
}
},
async loadLoadpoints() {
const response = await api.get("/config/loadpoints");
this.loadpoints = response.data?.result || [];
this.loadpoints = response.data || [];
},
getMetersByNames(names) {
if (!names || !this.meters) {
@ -865,7 +865,7 @@ export default {
try {
const response = await api.get(`/config/devices/${type}/${name}/status`);
if (!this.deviceValues[type]) this.deviceValues[type] = {};
this.deviceValues[type][name] = response.data.result;
this.deviceValues[type][name] = response.data;
} catch (error) {
console.error("Error fetching device values for", type, name, error);
return null;

View file

@ -229,7 +229,7 @@ export default {
count: showAll ? null : DEFAULT_COUNT,
},
});
this.lines = response.data?.result || [];
this.lines = response.data || [];
this.$nextTick(() => {
if (showAll) {
this.scrollToTop();
@ -258,7 +258,7 @@ export default {
async updateAreas() {
try {
const response = await api.get("/system/log/areas");
this.availableAreas = response.data?.result || [];
this.availableAreas = response.data || [];
} catch (e) {
console.error(e);
}

View file

@ -673,7 +673,7 @@ export default {
async loadSessions() {
const response = await api.get("sessions");
// ensure sessions are sorted by created date
const sortedSessions = response.data?.result.sort((a, b) => {
const sortedSessions = response.data.sort((a, b) => {
return new Date(a.created) - new Date(b.created);
});
this.sessions = sortedSessions;

View file

@ -3,19 +3,13 @@ package main
import (
"encoding/json"
"log"
"net/url"
"os"
"github.com/getkin/kin-openapi/openapi3"
)
func main() {
uri, err := url.Parse(os.Args[1])
if err != nil {
log.Fatal(err)
}
doc, err := openapi3.NewLoader().LoadFromURI(uri)
doc, err := openapi3.NewLoader().LoadFromFile(os.Args[1])
if err != nil {
log.Fatal("failed to load OpenAPI spec:", err)
}
@ -28,9 +22,7 @@ func main() {
log.Fatal(err)
}
if err := os.WriteFile("openapi.json", b, 0o644); err != nil {
if err := os.WriteFile(os.Args[2], b, 0o644); err != nil {
log.Fatal(err)
}
log.Println("OpenAPI spec written to openapi.json")
}

1
go.mod
View file

@ -240,6 +240,7 @@ require (
tool (
github.com/dmarkham/enumer
github.com/evcc-io/evcc/cmd/decorate
github.com/evcc-io/evcc/cmd/openapi
github.com/gokrazy/tools/cmd/gok
go.uber.org/mock/mockgen
)

View file

@ -73,7 +73,7 @@ func devicesConfigHandler(w http.ResponseWriter, r *http.Request) {
return
}
jsonResult(w, res)
jsonWrite(w, res)
}
func deviceConfigMap[T any](class templates.Class, dev config.Device[T]) (map[string]any, error) {
@ -196,7 +196,7 @@ func deviceConfigHandler(w http.ResponseWriter, r *http.Request) {
// TODO return application/yaml content type if type != template
jsonResult(w, res)
jsonWrite(w, res)
}
func deviceStatus[T any](name string, h config.Handler[T]) (T, error) {
@ -242,7 +242,7 @@ func deviceStatusHandler(w http.ResponseWriter, r *http.Request) {
return
}
jsonResult(w, testInstance(instance))
jsonWrite(w, testInstance(instance))
}
func newDevice[T any](ctx context.Context, class templates.Class, req configReq, newFromConf newFromConfFunc[T], h config.Handler[T]) (*config.Config, error) {
@ -313,7 +313,7 @@ func newDeviceHandler(w http.ResponseWriter, r *http.Request) {
Name: config.NameForID(conf.ID),
}
jsonResult(w, res)
jsonWrite(w, res)
}
func updateDevice[T any](ctx context.Context, id int, class templates.Class, req configReq, newFromConf newFromConfFunc[T], h config.Handler[T]) error {
@ -387,7 +387,7 @@ func updateDeviceHandler(w http.ResponseWriter, r *http.Request) {
ID: id,
}
jsonResult(w, res)
jsonWrite(w, res)
}
func configurableDevice[T any](name string, h config.Handler[T]) (config.ConfigurableDevice[T], error) {
@ -529,7 +529,7 @@ func deleteDeviceHandler(site site.API) func(w http.ResponseWriter, r *http.Requ
ID: id,
}
jsonResult(w, res)
jsonWrite(w, res)
}
}
@ -595,5 +595,5 @@ func testConfigHandler(w http.ResponseWriter, r *http.Request) {
// prevent context from being cancelled
close(done)
jsonResult(w, testInstance(instance))
jsonWrite(w, testInstance(instance))
}

View file

@ -93,7 +93,7 @@ func loadpointsConfigHandler() http.HandlerFunc {
return loadpointConfig(dev)
})
jsonResult(w, res)
jsonWrite(w, res)
}
}
@ -118,7 +118,7 @@ func loadpointConfigHandler() http.HandlerFunc {
res := loadpointConfig(dev)
jsonResult(w, res)
jsonWrite(w, res)
}
}
@ -284,6 +284,6 @@ func deleteLoadpointHandler() http.HandlerFunc {
ID: id,
}
jsonResult(w, res)
jsonWrite(w, res)
}
}

View file

@ -39,7 +39,7 @@ func templatesHandler(w http.ResponseWriter, r *http.Request) {
return
}
jsonResult(w, res)
jsonWrite(w, res)
return
}
@ -56,7 +56,7 @@ func templatesHandler(w http.ResponseWriter, r *http.Request) {
res = append(res, t)
}
jsonResult(w, res)
jsonWrite(w, res)
}
// productsHandler returns the list of products by class
@ -101,5 +101,5 @@ func productsHandler(w http.ResponseWriter, r *http.Request) {
return strings.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name))
})
jsonResult(w, res)
jsonWrite(w, res)
}

View file

@ -26,7 +26,7 @@ func siteHandler(site site.API) http.HandlerFunc {
Ext: site.GetExtMeterRefs(),
}
jsonResult(w, res)
jsonWrite(w, res)
}
}

View file

@ -30,11 +30,11 @@ func updateSponsortokenHandler(w http.ResponseWriter, r *http.Request) {
settings.SetString(keys.SponsorToken, req.Token)
setConfigDirty()
jsonResult(w, sponsor.Status())
jsonWrite(w, sponsor.Status())
}
func deleteSponsorTokenHandler(w http.ResponseWriter, r *http.Request) {
settings.SetString(keys.SponsorToken, "")
setConfigDirty()
jsonResult(w, true)
jsonWrite(w, true)
}

View file

@ -17,14 +17,14 @@ import (
func settingsGetStringHandler(key string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
res, _ := settings.String(key)
jsonResult(w, res)
jsonWrite(w, res)
}
}
func settingsDeleteHandler(key string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
settings.SetString(key, "")
jsonResult(w, true)
jsonWrite(w, true)
}
}
@ -41,7 +41,7 @@ func settingsSetDurationHandler(key string) http.HandlerFunc {
settings.SetInt(key, int64(time.Second*time.Duration(val)))
setConfigDirty()
jsonResult(w, val)
jsonWrite(w, val)
}
}
@ -67,7 +67,7 @@ func settingsSetYamlHandler(key string, other, struc any) http.HandlerFunc {
settings.SetString(key, val)
setConfigDirty()
jsonResult(w, val)
jsonWrite(w, val)
}
}
@ -94,7 +94,7 @@ func settingsSetJsonHandler(key string, valueChan chan<- util.Param, newStruc fu
valueChan <- util.Param{Key: key, Val: struc}
jsonResult(w, true)
jsonWrite(w, true)
}
}
@ -105,6 +105,6 @@ func settingsDeleteJsonHandler(key string, valueChan chan<- util.Param, struc an
valueChan <- util.Param{Key: key, Val: struc}
jsonResult(w, true)
jsonWrite(w, true)
}
}

View file

@ -37,7 +37,7 @@ func remoteDemandHandler(lp loadpoint.API) http.HandlerFunc {
Demand: demand,
}
jsonResult(w, res)
jsonWrite(w, res)
}
}
@ -69,7 +69,7 @@ func planHandler(lp loadpoint.API) http.HandlerFunc {
Power: maxPower,
}
jsonResult(w, res)
jsonWrite(w, res)
}
}
@ -131,7 +131,7 @@ func staticPlanPreviewHandler(lp loadpoint.API) http.HandlerFunc {
Power: maxPower,
}
jsonResult(w, res)
jsonWrite(w, res)
}
}
@ -189,7 +189,7 @@ func repeatingPlanPreviewHandler(lp loadpoint.API) http.HandlerFunc {
Power: maxPower,
}
jsonResult(w, res)
jsonWrite(w, res)
}
}
@ -234,7 +234,7 @@ func planEnergyHandler(lp loadpoint.API) http.HandlerFunc {
Time: ts,
}
jsonResult(w, res)
jsonWrite(w, res)
}
}
@ -247,7 +247,7 @@ func planRemoveHandler(lp loadpoint.API) http.HandlerFunc {
}
res := struct{}{}
jsonResult(w, res)
jsonWrite(w, res)
}
}
@ -271,7 +271,7 @@ func vehicleSelectHandler(site site.API, lp loadpoint.API) http.HandlerFunc {
Vehicle: v.GetTitle(),
}
jsonResult(w, res)
jsonWrite(w, res)
}
}
@ -280,7 +280,7 @@ func vehicleRemoveHandler(lp loadpoint.API) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
lp.SetVehicle(nil)
res := struct{}{}
jsonResult(w, res)
jsonWrite(w, res)
}
}
@ -289,6 +289,6 @@ func vehicleDetectHandler(lp loadpoint.API) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
lp.StartVehicleDetection()
res := struct{}{}
jsonResult(w, res)
jsonWrite(w, res)
}
}

View file

@ -87,7 +87,7 @@ func sessionHandler(w http.ResponseWriter, r *http.Request) {
return
}
jsonResult(w, res)
jsonWrite(w, res)
}
// deleteSessionHandler removes session in sessions table with given id
@ -107,7 +107,7 @@ func deleteSessionHandler(w http.ResponseWriter, r *http.Request) {
return
}
jsonResult(w, res)
jsonWrite(w, res)
}
// updateSessionHandler updates the data of an existing session

View file

@ -81,16 +81,12 @@ func jsonHandler(h http.Handler) http.Handler {
})
}
func jsonWrite(w http.ResponseWriter, content interface{}) {
if err := json.NewEncoder(w).Encode(content); err != nil {
func jsonWrite(w http.ResponseWriter, data interface{}) {
if err := json.NewEncoder(w).Encode(data); err != nil {
log.ERROR.Printf("httpd: failed to encode JSON: %v", err)
}
}
func jsonResult(w http.ResponseWriter, res interface{}) {
jsonWrite(w, map[string]interface{}{"result": res})
}
func jsonError(w http.ResponseWriter, status int, err error) {
w.WriteHeader(status)
@ -129,7 +125,7 @@ func handler[T any](conv func(string) (T, error), set func(T) error, get func()
return
}
jsonResult(w, get())
jsonWrite(w, get())
}
}
@ -175,7 +171,7 @@ func durationHandler(set func(time.Duration) error, get func() time.Duration) ht
// getHandler returns api results
func getHandler[T any](get func() T) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
jsonResult(w, get())
jsonWrite(w, get())
}
}
@ -199,7 +195,7 @@ func updateSmartCostLimit(site site.API, setLimit func(loadpoint.API, *float64))
setLimit(lp, val)
}
jsonResult(w, val)
jsonWrite(w, val)
}
}
@ -221,7 +217,7 @@ func updateBatteryMode(site site.API) http.HandlerFunc {
site.SetBatteryModeExternal(val)
jsonResult(w, site.GetBatteryModeExternal())
jsonWrite(w, site.GetBatteryModeExternal())
}
}
@ -258,7 +254,7 @@ func stateHandler(cache *util.ParamCache) http.HandlerFunc {
return
}
jsonResult(w, res)
jsonWrite(w, res)
}
}
@ -306,7 +302,7 @@ func tariffHandler(site site.API) http.HandlerFunc {
Rates: rates,
}
jsonResult(w, res)
jsonWrite(w, res)
}
}
@ -318,12 +314,12 @@ func socketHandler(hub *SocketHub) http.HandlerFunc {
}
func logAreasHandler(w http.ResponseWriter, r *http.Request) {
jsonResult(w, logstash.Areas())
jsonWrite(w, logstash.Areas())
}
func clearCacheHandler(w http.ResponseWriter, r *http.Request) {
util.ResetCached()
jsonResult(w, "OK")
jsonWrite(w, "OK")
}
func logHandler(w http.ResponseWriter, r *http.Request) {
@ -351,7 +347,7 @@ func logHandler(w http.ResponseWriter, r *http.Request) {
return
}
jsonResult(w, log)
jsonWrite(w, log)
}
// adminPasswordValid validates the admin password and returns true if valid

View file

@ -36,7 +36,7 @@ func minSocHandler(site site.API) http.HandlerFunc {
Soc: v.GetMinSoc(),
}
jsonResult(w, res)
jsonWrite(w, res)
}
}
@ -65,7 +65,7 @@ func limitSocHandler(site site.API) http.HandlerFunc {
Soc: v.GetLimitSoc(),
}
jsonResult(w, res)
jsonWrite(w, res)
}
}
@ -116,7 +116,7 @@ func planSocHandler(site site.API) http.HandlerFunc {
Time: ts,
}
jsonResult(w, res)
jsonWrite(w, res)
}
}
@ -146,7 +146,7 @@ func addRepeatingPlansHandler(site site.API) http.HandlerFunc {
return
}
jsonResult(w, plansWrapper)
jsonWrite(w, plansWrapper)
}
}
@ -167,6 +167,6 @@ func planSocRemoveHandler(site site.API) http.HandlerFunc {
}
res := struct{}{}
jsonResult(w, res)
jsonWrite(w, res)
}
}

File diff suppressed because it is too large Load diff

3
server/openapi.go Normal file
View file

@ -0,0 +1,3 @@
package server
//go:generate go tool openapi openapi.yaml mcp/openapi.json

View file

@ -2,7 +2,7 @@ openapi: 3.0.3
info:
title: evcc
description: Solar charging. Super simple.
version: latest
version: "0.2.0"
contact:
url: https://github.com/evcc-io/evcc
servers: