refactor: Break apart monolithic configuration schema (#1977)

[skip ci]
This commit is contained in:
Dermot Duffy
2025-03-22 15:06:11 -07:00
committed by GitHub
parent 92fc8dd844
commit d6225fefda
266 changed files with 2789 additions and 2562 deletions
+7
View File
@@ -0,0 +1,7 @@
import { z } from 'zod';
import { actionsBaseSchema } from '../actions/types';
export const elementsBaseSchema = actionsBaseSchema.extend({
style: z.record(z.string().nullable().or(z.undefined()).or(z.number())).optional(),
title: z.string().nullable().optional(),
});
@@ -0,0 +1,15 @@
import { z } from 'zod';
import { MENU_PRIORITY_DEFAULT, MENU_PRIORITY_MAX } from '../../../common/const';
export const menuBaseSchema = z.object({
enabled: z.boolean().default(true).optional(),
priority: z
.number()
.min(0)
.max(MENU_PRIORITY_MAX)
.default(MENU_PRIORITY_DEFAULT)
.optional(),
alignment: z.enum(['matching', 'opposing']).default('matching').optional(),
icon: z.string().optional(),
permanent: z.boolean().default(false).optional(),
});
@@ -0,0 +1,8 @@
import { z } from 'zod';
import { iconSchema } from '../../stock/icon';
import { menuBaseSchema } from './base';
export const menuIconSchema = menuBaseSchema.merge(iconSchema).extend({
type: z.literal('custom:advanced-camera-card-menu-icon'),
});
export type MenuIcon = z.infer<typeof menuIconSchema>;
@@ -0,0 +1,11 @@
import { z } from 'zod';
import { stateIconSchema } from '../../stock/state-icon';
import { menuBaseSchema } from './base';
export const menuStateIconSchema = menuBaseSchema
.merge(stateIconSchema)
.extend({
type: z.literal('custom:advanced-camera-card-menu-state-icon'),
})
.merge(menuBaseSchema);
export type MenuStateIcon = z.infer<typeof menuStateIconSchema>;
@@ -0,0 +1,10 @@
import { z } from 'zod';
import { stateIconSchema } from '../../stock/state-icon';
import { menuBaseSchema } from './base';
import { menuSubmenuItemSchema } from './submenu';
export const menuSubmenuSelectSchema = menuBaseSchema.merge(stateIconSchema).extend({
type: z.literal('custom:advanced-camera-card-menu-submenu-select'),
options: z.record(menuSubmenuItemSchema.deepPartial()).optional(),
});
export type MenuSubmenuSelect = z.infer<typeof menuSubmenuSelectSchema>;
@@ -0,0 +1,20 @@
import { z } from 'zod';
import { elementsBaseSchema } from '../../base';
import { iconSchema } from '../../stock/icon';
import { menuBaseSchema } from './base';
export const menuSubmenuItemSchema = elementsBaseSchema.extend({
entity: z.string().optional(),
icon: z.string().optional(),
state_color: z.boolean().default(true),
selected: z.boolean().default(false),
subtitle: z.string().optional(),
enabled: z.boolean().default(true),
});
export type MenuSubmenuItem = z.infer<typeof menuSubmenuItemSchema>;
export const menuSubmenuSchema = menuBaseSchema.merge(iconSchema).extend({
type: z.literal('custom:advanced-camera-card-menu-submenu'),
items: menuSubmenuItemSchema.array(),
});
export type MenuSubmenu = z.infer<typeof menuSubmenuSchema>;
@@ -0,0 +1,6 @@
import { MenuIcon } from './icon';
import { MenuStateIcon } from './state-icon';
import { MenuSubmenu } from './submenu';
import { MenuSubmenuSelect } from './submenu-select';
export type MenuItem = MenuIcon | MenuStateIcon | MenuSubmenu | MenuSubmenuSelect;
@@ -0,0 +1,17 @@
import { z } from 'zod';
// https://www.home-assistant.io/lovelace/picture-elements/#custom-elements
export const customSchema = z
.object({
// Insist that Advanced Camera Card custom elements are handled by other schemas.
type: z.string().superRefine((val, ctx) => {
if (!val.match(/^custom:(?!advanced-camera-card).+/)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'advanced-camera-card custom elements must match specific schemas',
fatal: true,
});
}
}),
})
.passthrough();
+9
View File
@@ -0,0 +1,9 @@
import { z } from 'zod';
import { elementsBaseSchema } from '../base';
// https://www.home-assistant.io/lovelace/picture-elements/#icon-element
export const iconSchema = elementsBaseSchema.extend({
type: z.literal('icon'),
icon: z.string(),
entity: z.string().optional(),
});
+15
View File
@@ -0,0 +1,15 @@
import { z } from 'zod';
import { elementsBaseSchema } from '../base';
// https://www.home-assistant.io/lovelace/picture-elements/#image-element
export const imageSchema = elementsBaseSchema.extend({
type: z.literal('image'),
entity: z.string().optional(),
image: z.string().optional(),
camera_image: z.string().optional(),
camera_view: z.string().optional(),
state_image: z.object({}).passthrough().optional(),
filter: z.string().optional(),
state_filter: z.object({}).passthrough().optional(),
aspect_ratio: z.string().optional(),
});
@@ -0,0 +1,12 @@
import { z } from 'zod';
import { elementsBaseSchema } from '../base';
// https://www.home-assistant.io/lovelace/picture-elements/#service-call-button
export const serviceCallButtonSchema = elementsBaseSchema.extend({
type: z.literal('service-button'),
// Title is required for service button.
title: z.string(),
service: z.string(),
service_data: z.object({}).passthrough().optional(),
});
@@ -0,0 +1,8 @@
import { z } from 'zod';
import { elementsBaseSchema } from '../base';
// https://www.home-assistant.io/lovelace/picture-elements/#state-badge
export const stateBadgeIconSchema = elementsBaseSchema.extend({
type: z.literal('state-badge'),
entity: z.string(),
});
@@ -0,0 +1,10 @@
import { z } from 'zod';
import { elementsBaseSchema } from '../base';
// https://www.home-assistant.io/lovelace/picture-elements/#state-icon
export const stateIconSchema = elementsBaseSchema.extend({
type: z.literal('state-icon'),
entity: z.string(),
icon: z.string().optional(),
state_color: z.boolean().default(true),
});
@@ -0,0 +1,11 @@
import { z } from 'zod';
import { elementsBaseSchema } from '../base';
// https://www.home-assistant.io/lovelace/picture-elements/#state-label
export const stateLabelSchema = elementsBaseSchema.extend({
type: z.literal('state-label'),
entity: z.string(),
attribute: z.string().optional(),
prefix: z.string().optional(),
suffix: z.string().optional(),
});
+81
View File
@@ -0,0 +1,81 @@
import { z } from 'zod';
import {
statusBarIconItemSchema,
statusBarImageItemSchema,
statusBarStringItemSchema,
} from '../actions/types';
import { StockCondition, stockConditionSchema } from '../conditions/stock/types';
import {
AdvancedCameraCardCondition,
advancedCameraCardConditionSchema,
} from '../conditions/types';
import { menuIconSchema } from './custom/menu/icon';
import { menuStateIconSchema } from './custom/menu/state-icon';
import { menuSubmenuSchema } from './custom/menu/submenu';
import { menuSubmenuSelectSchema } from './custom/menu/submenu-select';
import { customSchema } from './stock/custom';
import { iconSchema } from './stock/icon';
import { imageSchema } from './stock/image';
import { serviceCallButtonSchema } from './stock/service-call';
import { stateBadgeIconSchema } from './stock/state-badge';
import { stateIconSchema } from './stock/state-icon';
import { stateLabelSchema } from './stock/state-label';
// Condition elements are included in this upper file as they recursively
// include other elements. Putting these elements elsewhere would cause
// typescript circular dependency errors as the types need to be both included
// in the master pictureElementSchema, but also refer to it internally.
//
// Provide a manual type definition to avoid the `any` that would be created by
// the lazy() evaluation below.
// See: https://zod.dev/?id=recursive-types
// https://www.home-assistant.io/lovelace/picture-elements/#image-element
type Conditional = {
type: 'conditional';
conditions: StockCondition[];
elements?: PictureElements;
};
export const conditionalSchema: z.ZodSchema<Conditional, z.ZodTypeDef> = z.object({
type: z.literal('conditional'),
conditions: stockConditionSchema.array(),
elements: z.lazy(() => pictureElementsSchema),
});
export type AdvancedCameraCardConditional = {
type: 'custom:advanced-camera-card-conditional';
conditions: AdvancedCameraCardCondition[];
elements?: PictureElements;
};
const advancedCameraCardConditionalSchema: z.ZodSchema<
AdvancedCameraCardConditional,
z.ZodTypeDef
> = z.object({
type: z.literal('custom:advanced-camera-card-conditional'),
conditions: advancedCameraCardConditionSchema.array(),
elements: z.lazy(() => pictureElementsSchema),
});
// Cannot use discriminatedUnion since customSchema uses a superRefine, which
// causes false rejections.
const pictureElementSchema = z.union([
conditionalSchema,
customSchema,
advancedCameraCardConditionalSchema,
iconSchema,
imageSchema,
menuIconSchema,
menuStateIconSchema,
menuSubmenuSchema,
menuSubmenuSelectSchema,
serviceCallButtonSchema,
stateBadgeIconSchema,
stateIconSchema,
stateLabelSchema,
statusBarIconItemSchema,
statusBarImageItemSchema,
statusBarStringItemSchema,
]);
export const pictureElementsSchema = pictureElementSchema.array().optional();
export type PictureElements = z.infer<typeof pictureElementsSchema>;