@lakutata/dbus 2.3.1 → 3.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +253 -25
  3. package/dist/cjs/adapters/DBusEntrypoint.d.ts +89 -0
  4. package/dist/cjs/adapters/DBusEntrypoint.js +280 -0
  5. package/dist/cjs/channel/DBusChannel.d.ts +36 -0
  6. package/dist/cjs/channel/DBusChannel.js +70 -0
  7. package/dist/cjs/components/DBusClientComponent.d.ts +494 -0
  8. package/dist/cjs/components/DBusClientComponent.js +272 -0
  9. package/dist/cjs/components/DBusObjectManager.d.ts +164 -0
  10. package/dist/cjs/components/DBusObjectManager.js +200 -0
  11. package/dist/cjs/context/DBusContext.d.ts +52 -0
  12. package/dist/cjs/context/DBusContext.js +68 -0
  13. package/dist/cjs/decorators/DBusInterface.d.ts +74 -0
  14. package/dist/cjs/decorators/DBusInterface.js +63 -0
  15. package/dist/cjs/decorators/DBusMethod.d.ts +115 -0
  16. package/dist/cjs/decorators/DBusMethod.js +20 -0
  17. package/dist/cjs/exports/DBus.d.ts +11 -0
  18. package/dist/cjs/exports/DBus.js +42 -0
  19. package/dist/cjs/lib/DBusClientOptions.d.ts +30 -0
  20. package/dist/cjs/lib/Declarations.d.ts +187 -0
  21. package/dist/cjs/lib/Declarations.js +113 -0
  22. package/dist/cjs/lib/Names.d.ts +35 -0
  23. package/dist/cjs/lib/Names.js +80 -0
  24. package/dist/cjs/lib/Signature.d.ts +14 -0
  25. package/dist/cjs/lib/Signature.js +53 -0
  26. package/dist/cjs/package.json +1 -0
  27. package/dist/cjs/types/DBusContextParams.d.ts +22 -0
  28. package/dist/cjs/types/DBusContextParams.js +2 -0
  29. package/dist/esm/adapters/DBusEntrypoint.js +274 -0
  30. package/dist/esm/channel/DBusChannel.js +67 -0
  31. package/dist/esm/components/DBusClientComponent.js +267 -0
  32. package/dist/esm/components/DBusObjectManager.js +197 -0
  33. package/dist/esm/context/DBusContext.js +64 -0
  34. package/dist/esm/decorators/DBusInterface.js +60 -0
  35. package/dist/esm/decorators/DBusMethod.js +17 -0
  36. package/dist/esm/exports/DBus.js +12 -0
  37. package/dist/esm/lib/DBusClientOptions.js +1 -0
  38. package/dist/esm/lib/Declarations.js +107 -0
  39. package/dist/esm/lib/Names.js +73 -0
  40. package/dist/esm/lib/Signature.js +49 -0
  41. package/dist/esm/types/DBusContextParams.js +1 -0
  42. package/dist/types/adapters/DBusEntrypoint.d.ts +89 -0
  43. package/dist/types/channel/DBusChannel.d.ts +36 -0
  44. package/dist/types/components/DBusClientComponent.d.ts +494 -0
  45. package/dist/types/components/DBusObjectManager.d.ts +164 -0
  46. package/dist/types/context/DBusContext.d.ts +52 -0
  47. package/dist/types/decorators/DBusInterface.d.ts +74 -0
  48. package/dist/types/decorators/DBusMethod.d.ts +115 -0
  49. package/dist/types/exports/DBus.d.ts +11 -0
  50. package/dist/types/lib/DBusClientOptions.d.ts +30 -0
  51. package/dist/types/lib/Declarations.d.ts +187 -0
  52. package/dist/types/lib/Names.d.ts +35 -0
  53. package/dist/types/lib/Signature.d.ts +14 -0
  54. package/dist/types/types/DBusContextParams.d.ts +22 -0
  55. package/package.json +38 -39
  56. package/CHANGELOG.md +0 -153
  57. package/dist/CommonExports.d.ts +0 -2
  58. package/dist/CommonExports.d.ts.map +0 -1
  59. package/dist/CommonExports.js +0 -44
  60. package/dist/ComponentExports.d.ts +0 -2
  61. package/dist/ComponentExports.d.ts.map +0 -1
  62. package/dist/ComponentExports.js +0 -5
  63. package/dist/ProviderExports.d.ts +0 -2
  64. package/dist/ProviderExports.d.ts.map +0 -1
  65. package/dist/ProviderExports.js +0 -5
  66. package/dist/components/DBusClientComponent.d.ts +0 -75
  67. package/dist/components/DBusClientComponent.d.ts.map +0 -1
  68. package/dist/components/DBusClientComponent.js +0 -125
  69. package/dist/interfaces/DBusClientOptions.d.ts +0 -7
  70. package/dist/interfaces/DBusClientOptions.d.ts.map +0 -1
  71. package/dist/providers/DBusClientProvider.d.ts +0 -75
  72. package/dist/providers/DBusClientProvider.d.ts.map +0 -1
  73. package/dist/providers/DBusClientProvider.js +0 -125
  74. package/dist/tests/DBusTest.spec.d.ts +0 -2
  75. package/dist/tests/DBusTest.spec.d.ts.map +0 -1
  76. package/dist/tests/DBusTest.spec.js +0 -26
  77. package/dist/tests/Test.d.ts +0 -9
  78. package/dist/tests/Test.d.ts.map +0 -1
  79. package/dist/tests/Test.js +0 -80
  80. package/tsconfig.json +0 -7
  81. /package/dist/{interfaces → cjs/lib}/DBusClientOptions.js +0 -0
@@ -0,0 +1,197 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { Component } from '@lakutata/core';
8
+ import { Autoload, Singleton } from '@lakutata/core/decorator/di';
9
+ import { checkPathTemplate } from '../lib/Names.js';
10
+ export const ATTACH = Symbol('DBUS.OBJECT.MANAGER.ATTACH');
11
+ export const STORE = Symbol('DBUS.OBJECT.MANAGER.STORE');
12
+ /**
13
+ * The application's D-Bus objects: it adds and removes the objects of the path templates of `@DBusInterface`
14
+ * (`/com/example/Devices/{sn}`), keeps the values of their properties, and emits their signals. Inject it
15
+ * (`@Inject(DBusObjectManager)`) or get it (`getObject(DBusObjectManager)`) from any object: it is autoloaded, a
16
+ * singleton of the application, and needs no registration.
17
+ *
18
+ * Its state lives in the application, not on the bus: the objects added before a {@link dbusEntrypoint} starts are
19
+ * exported when it starts, and every running D-Bus adapter whose interfaces match a path exports the object. The
20
+ * clients are told of the objects added and removed by the `org.freedesktop.DBus.ObjectManager` interface of the root
21
+ * object (`InterfacesAdded`, `InterfacesRemoved`).
22
+ *
23
+ * It emits `propertyWritten` (a {@link DBusPropertyWrite}) when a client writes a writable property.
24
+ * @example
25
+ * ```typescript
26
+ * import {Component} from 'lakutata'
27
+ * import {Inject} from 'lakutata/decorator/di'
28
+ * import {DBusObjectManager, defineDBusProperty, defineDBusSignal, type DBusPropertyWrite} from '@lakutata/dbus'
29
+ *
30
+ * export const DeviceState = defineDBusProperty<string>('State', 's', {initial: 'idle'})
31
+ * export const DeviceLevel = defineDBusProperty<number>('Level', 'i', {writable: true, initial: 1})
32
+ * export const DeviceRemoved = defineDBusSignal<string>('Removed', 's')
33
+ *
34
+ * //Declared in the components of the application, with the controllers of the interfaces at /com/example/Devices/{sn}
35
+ * export class Devices extends Component {
36
+ * @Inject(DBusObjectManager)
37
+ * protected readonly manager: DBusObjectManager
38
+ *
39
+ * protected async init(): Promise<void> {
40
+ * this.manager.on('propertyWritten', (write: DBusPropertyWrite): void => {
41
+ * if (write.property === DeviceLevel) console.log(`${write.path}: level ${write.value}`)
42
+ * })
43
+ * }
44
+ *
45
+ * public plug(sn: string): void {
46
+ * const path: string = `/com/example/Devices/${sn}`
47
+ * this.manager.addObject(path)
48
+ * this.manager.setPropertyValue(path, DeviceState, 'running')
49
+ * }
50
+ *
51
+ * public unplug(sn: string): void {
52
+ * const path: string = `/com/example/Devices/${sn}`
53
+ * this.manager.emitSignal(path, DeviceRemoved, sn)
54
+ * this.manager.removeObject(path)
55
+ * }
56
+ * }
57
+ * ```
58
+ */
59
+ let DBusObjectManager = class DBusObjectManager extends Component {
60
+ #objects = new Set();
61
+ #values = new Map();
62
+ #sinks = new Set();
63
+ /**
64
+ * Send the changes to an adapter until the returned function is called; the objects already added are exported by it
65
+ * @param sink
66
+ */
67
+ [ATTACH](sink) {
68
+ this.#sinks.add(sink);
69
+ this.#objects.forEach((path) => sink.addObject(path));
70
+ return () => void this.#sinks.delete(sink);
71
+ }
72
+ /**
73
+ * Keep a value written by a client
74
+ * @param write
75
+ */
76
+ [STORE](write) {
77
+ this.#valuesOf(write.path).set(write.property, write.value);
78
+ this.emit('propertyWritten', write);
79
+ }
80
+ /**
81
+ * The values of the properties of an object
82
+ * @param path
83
+ */
84
+ #valuesOf(path) {
85
+ let values = this.#values.get(path);
86
+ if (!values) {
87
+ values = new Map();
88
+ this.#values.set(path, values);
89
+ }
90
+ return values;
91
+ }
92
+ /**
93
+ * Add an object: every running D-Bus adapter whose interfaces have a path template matching it (or the same path)
94
+ * exports it, with those interfaces, and the adapters started later export it when they start. The variables of the
95
+ * matching templates are given to the methods called on it. An object whose path matches no template is kept, but
96
+ * not exported.
97
+ * @param path The object path (`'/com/example/Devices/A1'`), not a template.
98
+ * @returns `false` when the object was already added, `true` otherwise.
99
+ * @throws {TypeError} When the path is invalid, or is a template (it contains braces).
100
+ */
101
+ addObject(path) {
102
+ checkPathTemplate(path);
103
+ if (/[{}]/.test(path))
104
+ throw new TypeError(`The object path "${path}" is a template, not the path of an object`);
105
+ if (this.#objects.has(path))
106
+ return false;
107
+ this.#objects.add(path);
108
+ this.#sinks.forEach((sink) => sink.addObject(path));
109
+ return true;
110
+ }
111
+ /**
112
+ * Remove an object added with {@link addObject}: the adapters stop exporting it, and its property values are
113
+ * forgotten (added again, it starts from the initial values). A fixed object (the path of a template without
114
+ * variables, exported by the adapters themselves) stays exported, with its property values, when it was also
115
+ * added: only its entry in {@link objects} is removed.
116
+ * @param path The object path given to {@link addObject}.
117
+ * @returns `false` when the object was not added, `true` otherwise.
118
+ */
119
+ removeObject(path) {
120
+ if (!this.#objects.delete(path))
121
+ return false;
122
+ let fixed = false;
123
+ this.#sinks.forEach((sink) => {
124
+ //An adapter keeps the objects it exports by itself
125
+ if (sink.isFixed(path))
126
+ fixed = true;
127
+ else
128
+ sink.removeObject(path);
129
+ });
130
+ if (!fixed)
131
+ this.#values.delete(path);
132
+ return true;
133
+ }
134
+ /**
135
+ * Tell whether an object was added with {@link addObject} and not removed since.
136
+ * @param path The object path.
137
+ * @returns `true` when the object is added.
138
+ */
139
+ hasObject(path) {
140
+ return this.#objects.has(path);
141
+ }
142
+ /**
143
+ * List the objects added with {@link addObject}, in the order they were added; the objects of the paths without
144
+ * variables, exported by the adapters themselves, are not listed.
145
+ * @returns The object paths.
146
+ */
147
+ objects() {
148
+ return [...this.#objects];
149
+ }
150
+ /**
151
+ * Set the value of a property on an object, validated by the property's schema: the clients read it from then on,
152
+ * and the running adapters exporting the object announce it (`PropertiesChanged`). The value is kept even when no
153
+ * adapter exports the object yet.
154
+ * @param path The object path (`'/com/example/Devices/A1'`).
155
+ * @param property The property's definition, listed in the `properties` of the object's interface.
156
+ * @param value The new value.
157
+ * @throws {InvalidValueException} When the value does not match the property's schema; nothing is changed.
158
+ */
159
+ setPropertyValue(path, property, value) {
160
+ const validated = property.validate(value);
161
+ this.#valuesOf(path).set(property, validated);
162
+ this.#sinks.forEach((sink) => sink.propertyChanged(path, property, validated));
163
+ }
164
+ /**
165
+ * Get the value of a property on an object: the last value set by the application or written by a client.
166
+ * @param path The object path.
167
+ * @param property The property's definition.
168
+ * @returns The value, or the property's `initial` value until one is set.
169
+ */
170
+ getPropertyValue(path, property) {
171
+ const values = this.#values.get(path);
172
+ return values?.has(property) ? values.get(property) : property.initial;
173
+ }
174
+ /**
175
+ * Emit a signal from an object, through every running adapter exporting the object with an interface listing the
176
+ * signal. The data is validated by the signal's DTO first.
177
+ * @param path The object path (`'/com/example/Devices/A1'`).
178
+ * @param signal The signal's definition, listed in the `signals` of the object's interface.
179
+ * @param data The data of the signal.
180
+ * @returns `false` when no running adapter exports the object with this signal (nothing is sent), `true` otherwise.
181
+ * @throws {InvalidValueException} When the data does not match the signal's DTO.
182
+ */
183
+ emitSignal(path, signal, data) {
184
+ const validated = signal.validate(data);
185
+ let emitted = false;
186
+ this.#sinks.forEach((sink) => {
187
+ if (sink.emitSignal(path, signal, validated))
188
+ emitted = true;
189
+ });
190
+ return emitted;
191
+ }
192
+ };
193
+ DBusObjectManager = __decorate([
194
+ Autoload(),
195
+ Singleton()
196
+ ], DBusObjectManager);
197
+ export { DBusObjectManager };
@@ -0,0 +1,64 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ import { DTO } from '@lakutata/core';
11
+ import { Expect } from '@lakutata/core/decorator/dto';
12
+ import { BaseContext } from '@lakutata/entrypoint';
13
+ /**
14
+ * The `type` of every {@link DBusContext}: `'DBUS'`. A middleware or an access rule shared by several transports tells
15
+ * the D-Bus calls apart with it.
16
+ */
17
+ export const DBUS_CONTEXT_TYPE = 'DBUS';
18
+ /**
19
+ * The context of a D-Bus method call: `this.context` of a controller action declared with `@DBusMethod` when a client
20
+ * calls it through {@link dbusEntrypoint}. The adapter creates one per call; its `data` is the call's input (the entries
21
+ * of its `a{sv}` dictionary). The variables of the object path template are not in the context: they are merged into
22
+ * the action's input, as the route parameters of HTTP.
23
+ * @example
24
+ * ```typescript
25
+ * import {Controller} from 'lakutata/com/entrypoint'
26
+ * import {DBusContext, DBusInterface, DBusMethod} from '@lakutata/dbus'
27
+ *
28
+ * @DBusInterface('com.example.Manager', {path: '/com/example/Manager'})
29
+ * class ManagerController extends Controller {
30
+ * //Returns "com.example.Manager.Ping at /com/example/Manager"
31
+ * @DBusMethod('Ping', {returns: 's'})
32
+ * public async ping(): Promise<string> {
33
+ * const context: DBusContext = this.context as DBusContext
34
+ * return `${context.interface}.${context.member} at ${context.path}`
35
+ * }
36
+ * }
37
+ * ```
38
+ */
39
+ export class DBusContext extends BaseContext {
40
+ /**
41
+ * Create the context of a method call, validating its properties.
42
+ * @param params The object path, the interface, the method and the input of the call.
43
+ * @throws {InvalidValueException} When a property is missing or invalid.
44
+ */
45
+ constructor(params) {
46
+ super(params);
47
+ }
48
+ }
49
+ __decorate([
50
+ Expect(DTO.String().valid(DBUS_CONTEXT_TYPE).default(DBUS_CONTEXT_TYPE)),
51
+ __metadata("design:type", String)
52
+ ], DBusContext.prototype, "type", void 0);
53
+ __decorate([
54
+ Expect(DTO.String().required()),
55
+ __metadata("design:type", String)
56
+ ], DBusContext.prototype, "path", void 0);
57
+ __decorate([
58
+ Expect(DTO.String().required()),
59
+ __metadata("design:type", String)
60
+ ], DBusContext.prototype, "interface", void 0);
61
+ __decorate([
62
+ Expect(DTO.String().required()),
63
+ __metadata("design:type", String)
64
+ ], DBusContext.prototype, "member", void 0);
@@ -0,0 +1,60 @@
1
+ import { setPrefix } from '@lakutata/entrypoint/channel';
2
+ import { setDBusInterfaceDeclaration } from '../lib/Declarations.js';
3
+ /**
4
+ * Serve a controller as a D-Bus interface: its `@DBusMethod` actions are the interface's methods, and the signals and
5
+ * properties of the options are its other members, on the objects at `options.path`. The controllers declaring the same
6
+ * path are served by the same objects, each one interface; a subclass inherits the declaration of its parent class.
7
+ * The objects also implement `org.freedesktop.DBus.Properties`, `Introspectable` and `Peer`, and the root object `/`
8
+ * implements `ObjectManager`. Nothing is served until a {@link dbusEntrypoint} runs.
9
+ * @param name The name of the interface (`'com.example.Device'`): two or more elements separated by dots, made of
10
+ * letters, digits and `_`, not starting with a digit; 255 characters at most.
11
+ * @param options The object path (or template) of the interface, its signals and its properties.
12
+ * @returns The class decorator.
13
+ * @throws {TypeError} When the class is declared, if the name of the interface or the path is invalid.
14
+ * @example
15
+ * ```typescript
16
+ * import {DTO} from 'lakutata'
17
+ * import {Controller} from 'lakutata/com/entrypoint'
18
+ * import {Expect, Return} from 'lakutata/decorator/dto'
19
+ * import {DBusInterface, DBusMethod, defineDBusProperty, defineDBusSignal} from '@lakutata/dbus'
20
+ *
21
+ * class DeviceSnDTO extends DTO {
22
+ * @Expect(DTO.String().required())
23
+ * public sn: string
24
+ * }
25
+ *
26
+ * class StatusDTO extends DTO {
27
+ * @Expect(DTO.String().required())
28
+ * public sn: string
29
+ *
30
+ * @Expect(DTO.Boolean().required())
31
+ * public online: boolean
32
+ * }
33
+ *
34
+ * export const DeviceChanged = defineDBusSignal('Changed', StatusDTO)
35
+ * export const DeviceState = defineDBusProperty<string>('State', 's', {initial: 'idle'})
36
+ *
37
+ * //Served by the objects /com/example/Devices/A1, /com/example/Devices/B2… added with DBusObjectManager
38
+ * @DBusInterface('com.example.Device', {path: '/com/example/Devices/{sn}', signals: [DeviceChanged], properties: [DeviceState]})
39
+ * export class DeviceController extends Controller {
40
+ * //The sn of the input is the one of the object path
41
+ * @DBusMethod('Status', DeviceSnDTO)
42
+ * @Return(StatusDTO)
43
+ * public async status(input: DeviceSnDTO): Promise<StatusDTO> {
44
+ * return {sn: input.sn, online: true} as StatusDTO
45
+ * }
46
+ * }
47
+ * ```
48
+ */
49
+ export function DBusInterface(name, options) {
50
+ return (target) => {
51
+ setDBusInterfaceDeclaration(target, {
52
+ interface: name,
53
+ path: options.path,
54
+ signals: options.signals ?? [],
55
+ properties: options.properties ?? []
56
+ });
57
+ //The methods of the controller are addressed at its interface and its path
58
+ setPrefix(target, { dbus: { path: options.path, interface: name } });
59
+ };
60
+ }
@@ -0,0 +1,17 @@
1
+ import { getActionDTOAndOptions, registerControllerAction } from '@lakutata/entrypoint/channel';
2
+ import { DBusChannel } from '../channel/DBusChannel.js';
3
+ import { schemaSignature } from '../lib/Signature.js';
4
+ export function DBusMethod(name, dtoConstructorOrOptions, options) {
5
+ return (target, propertyKey, descriptor) => {
6
+ const isDTO = typeof dtoConstructorOrOptions === 'function';
7
+ const { returns, ...actionOptions } = (isDTO ? options : dtoConstructorOrOptions) ?? {};
8
+ const [dtoConstructor, validatedOptions] = isDTO
9
+ ? getActionDTOAndOptions(dtoConstructorOrOptions, actionOptions)
10
+ : getActionDTOAndOptions(actionOptions);
11
+ const pattern = { member: name };
12
+ if (returns !== undefined)
13
+ pattern.returns = typeof returns === 'string' ? returns : schemaSignature(returns);
14
+ registerControllerAction(DBusChannel, target, propertyKey, pattern, dtoConstructor, validatedOptions);
15
+ return descriptor;
16
+ };
17
+ }
@@ -0,0 +1,12 @@
1
+ //The DBus channel: the controllers served as the interfaces of the DBus objects
2
+ export { DBusChannel } from '../channel/DBusChannel.js';
3
+ export { DBusContext, DBUS_CONTEXT_TYPE } from '../context/DBusContext.js';
4
+ export { DBusInterface } from '../decorators/DBusInterface.js';
5
+ export { DBusMethod } from '../decorators/DBusMethod.js';
6
+ export { defineDBusSignal, defineDBusProperty } from '../lib/Declarations.js';
7
+ export { dbusEntrypoint } from '../adapters/DBusEntrypoint.js';
8
+ export { DBusObjectManager } from '../components/DBusObjectManager.js';
9
+ //The DBus client
10
+ export { DBusClientComponent, buildDBusClientOptions } from '../components/DBusClientComponent.js';
11
+ //The DBus SDK
12
+ export { DBus, DBusInterface as DBusRemoteInterface, DBusObject, DBusService, LocalInterface, LocalObject, LocalService, createDBusError, DBusSignedValue, DBusSignalEmitter, Signature, RequestNameFlags, RequestNameResultCode } from 'dbus-sdk';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,107 @@
1
+ import { DTO } from '@lakutata/core';
2
+ import { checkInterfaceName, checkMemberName, checkPathTemplate } from './Names.js';
3
+ import { schemaSignature } from './Signature.js';
4
+ /**
5
+ * Declare a D-Bus signal, to list in the `signals` of one or more `@DBusInterface`. The signal has one argument: a
6
+ * dictionary of variants (`a{sv}`) holding a DTO, or a value of the signature given. Its data is validated by its DTO
7
+ * when it is emitted.
8
+ * @typeParam T The data the signal carries.
9
+ * @param name The member name of the signal (`'Changed'`): letters, digits and `_`, not starting with a digit; 255
10
+ * characters at most.
11
+ * @param data The DTO of the signal's data, or the D-Bus signature of its argument (`'s'`, `'a{sv}'`…); with a
12
+ * signature, the data is not validated. `'a{sv}'` by default.
13
+ * @returns The signal's definition.
14
+ * @throws {TypeError} When the name is invalid.
15
+ * @example
16
+ * ```typescript
17
+ * import {DTO} from 'lakutata'
18
+ * import {Expect} from 'lakutata/decorator/dto'
19
+ * import {defineDBusSignal, type DBusObjectManager} from '@lakutata/dbus'
20
+ *
21
+ * class StatusDTO extends DTO {
22
+ * @Expect(DTO.String().required())
23
+ * public sn: string
24
+ *
25
+ * @Expect(DTO.Boolean().required())
26
+ * public online: boolean
27
+ * }
28
+ *
29
+ * //A dictionary of variants a{sv}, validated by StatusDTO
30
+ * export const DeviceChanged = defineDBusSignal<StatusDTO>('Changed', StatusDTO)
31
+ * //A string s
32
+ * export const DeviceMessage = defineDBusSignal<string>('Message', 's')
33
+ *
34
+ * declare const manager: DBusObjectManager
35
+ * manager.emitSignal('/com/example/Devices/A1', DeviceChanged, {sn: 'A1', online: true} as StatusDTO)
36
+ * manager.emitSignal('/com/example/Devices/A1', DeviceMessage, 'Rebooted')
37
+ * ```
38
+ */
39
+ export function defineDBusSignal(name, data = 'a{sv}') {
40
+ checkMemberName(name, 'signal');
41
+ const isDTO = typeof data === 'function';
42
+ return {
43
+ kind: 'signal',
44
+ name: name,
45
+ type: isDTO ? schemaSignature(data) : data,
46
+ validate: (value) => isDTO ? data.validate(value) : value
47
+ };
48
+ }
49
+ /**
50
+ * Declare a D-Bus property, to list in the `properties` of one or more `@DBusInterface`. Its values are kept by
51
+ * `DBusObjectManager`, one per object: the application sets them, the clients read them (`Get`, `GetAll`) and are told
52
+ * of their changes (`PropertiesChanged`), and write them when the property is writable.
53
+ * @typeParam T The values of the property.
54
+ * @param name The member name of the property (`'State'`): letters, digits and `_`, not starting with a digit; 255
55
+ * characters at most.
56
+ * @param type The D-Bus signature of its values (`'s'`, `'i'`, `'b'`, `'a{sv}'`…); it is not checked against `schema`.
57
+ * @param options Whether the clients may write it, its initial value and the schema of its values; none by default
58
+ * (read-only, no initial value, every value accepted).
59
+ * @returns The property's definition.
60
+ * @throws {TypeError} When the name is invalid.
61
+ * @example
62
+ * ```typescript
63
+ * import {DTO} from 'lakutata'
64
+ * import {defineDBusProperty, type DBusObjectManager, type DBusPropertyWrite} from '@lakutata/dbus'
65
+ *
66
+ * //Read-only, "idle" until it is set
67
+ * export const DeviceState = defineDBusProperty<string>('State', 's', {initial: 'idle'})
68
+ * //Written by the clients, from 0 to 10
69
+ * export const DeviceLevel = defineDBusProperty<number>('Level', 'i', {writable: true, initial: 1, schema: DTO.Number().integer().min(0).max(10)})
70
+ *
71
+ * declare const manager: DBusObjectManager
72
+ * manager.setPropertyValue('/com/example/Devices/A1', DeviceState, 'running')
73
+ * manager.on('propertyWritten', (write: DBusPropertyWrite): void => {
74
+ * if (write.property === DeviceLevel) console.log(`${write.path}: level ${write.value}`)
75
+ * })
76
+ * ```
77
+ */
78
+ export function defineDBusProperty(name, type, options = {}) {
79
+ checkMemberName(name, 'property');
80
+ const schema = options.schema;
81
+ return {
82
+ kind: 'property',
83
+ name: name,
84
+ type: type,
85
+ writable: options.writable === true,
86
+ initial: options.initial,
87
+ validate: (value) => schema ? DTO.validate(value, schema) : value
88
+ };
89
+ }
90
+ const DBUS_INTERFACE = Symbol('DBUS.INTERFACE');
91
+ /**
92
+ * Keep the declaration of the interface of a controller
93
+ * @param controller
94
+ * @param declaration
95
+ */
96
+ export function setDBusInterfaceDeclaration(controller, declaration) {
97
+ checkInterfaceName(declaration.interface);
98
+ checkPathTemplate(declaration.path);
99
+ Reflect.defineMetadata(DBUS_INTERFACE, declaration, controller);
100
+ }
101
+ /**
102
+ * The declaration of the interface of a controller, or of its nearest parent class declaring one
103
+ * @param controller
104
+ */
105
+ export function getDBusInterfaceDeclaration(controller) {
106
+ return Reflect.getMetadata(DBUS_INTERFACE, controller);
107
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * The names of the interfaces and of the errors: dot-separated elements
3
+ */
4
+ const INTERFACE_NAME = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)+$/;
5
+ /**
6
+ * The names of the members: the methods, the properties and the signals
7
+ */
8
+ const MEMBER_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
9
+ /**
10
+ * The object paths, their elements may be variables of a template ("/com/example/Devices/{sn}")
11
+ */
12
+ const PATH_TEMPLATE = /^\/$|^(\/([A-Za-z0-9_]+|\{[A-Za-z_][A-Za-z0-9_]*}))+$/;
13
+ /**
14
+ * Check the name of an interface
15
+ * @param name
16
+ */
17
+ export function checkInterfaceName(name) {
18
+ if (typeof name !== 'string' || name.length > 255 || !INTERFACE_NAME.test(name))
19
+ throw new TypeError(`Invalid DBus interface name "${name}": dot-separated elements of letters, digits and "_", such as "com.example.Device"`);
20
+ return name;
21
+ }
22
+ /**
23
+ * Check the name of a member
24
+ * @param name
25
+ * @param kind
26
+ */
27
+ export function checkMemberName(name, kind) {
28
+ if (typeof name !== 'string' || name.length > 255 || !MEMBER_NAME.test(name))
29
+ throw new TypeError(`Invalid DBus ${kind} name "${name}": letters, digits and "_", not starting with a digit`);
30
+ return name;
31
+ }
32
+ /**
33
+ * Check an object path, or a template of object paths
34
+ * @param path
35
+ */
36
+ export function checkPathTemplate(path) {
37
+ if (typeof path !== 'string' || !PATH_TEMPLATE.test(path))
38
+ throw new TypeError(`Invalid DBus object path "${path}": "/" separated elements of letters, digits and "_", or variables such as "{sn}"`);
39
+ return path;
40
+ }
41
+ /**
42
+ * Compile a template of object paths
43
+ * @param template
44
+ */
45
+ export function compilePathTemplate(template) {
46
+ const variables = [];
47
+ const expression = new RegExp(`^${checkPathTemplate(template).replace(/\{([A-Za-z_][A-Za-z0-9_]*)}/g, (matched, name) => {
48
+ variables.push(name);
49
+ return '([A-Za-z0-9_]+)';
50
+ })}$`);
51
+ return {
52
+ template: template,
53
+ variables: variables,
54
+ static: !variables.length,
55
+ match: (path) => {
56
+ const matched = expression.exec(path);
57
+ return matched ? Object.fromEntries(variables.map((name, index) => [name, matched[index + 1]])) : undefined;
58
+ }
59
+ };
60
+ }
61
+ /**
62
+ * The name of the DBus error of an exception: its errno, or its name
63
+ * @param error
64
+ */
65
+ export function errorName(error) {
66
+ const { errno, name } = (error && typeof error === 'object' ? error : {});
67
+ const candidate = String(errno ?? name ?? 'Error');
68
+ //A dotted name is a full DBus error name, the others are prefixed by the service name
69
+ if (INTERFACE_NAME.test(candidate))
70
+ return candidate;
71
+ const element = candidate.replace(/[^A-Za-z0-9_]/g, '_');
72
+ return /^[A-Za-z_]/.test(element) ? element : `E${element}`;
73
+ }
@@ -0,0 +1,49 @@
1
+ import { DBusSignedValue } from 'dbus-sdk';
2
+ /**
3
+ * The DBus signature of the values of a schema: an object is a dictionary of variants,
4
+ * the scalars have their own types, the other values are variants
5
+ * @param schema
6
+ */
7
+ export function schemaSignature(schema) {
8
+ const target = typeof schema === 'function' ? schema.Schema() : schema;
9
+ switch (target?.type) {
10
+ case 'object':
11
+ return 'a{sv}';
12
+ case 'string':
13
+ return 's';
14
+ case 'boolean':
15
+ return 'b';
16
+ case 'number':
17
+ return (target._rules ?? []).some((rule) => rule.name === 'integer') ? 'x' : 'd';
18
+ case 'bigint':
19
+ return 'x';
20
+ case 'array':
21
+ return 'av';
22
+ default:
23
+ return 'v';
24
+ }
25
+ }
26
+ /**
27
+ * A value ready to be encoded: the dates are ISO strings, the undefined and null values are removed (a variant has no null);
28
+ * the typed values of dbus-sdk (`DBusUInt32`, `DBusVariant`…) are kept as they are, a Map keeps its entries (its keys
29
+ * keep their types)
30
+ * @param value
31
+ */
32
+ export function toEncodable(value) {
33
+ if (value instanceof Date)
34
+ return value.toISOString();
35
+ if (value instanceof DBusSignedValue)
36
+ return value;
37
+ if (Array.isArray(value))
38
+ return value.filter((item) => item !== undefined && item !== null).map(toEncodable);
39
+ if (value instanceof Map)
40
+ return new Map([...value].filter(([, item]) => item !== undefined && item !== null).map(([key, item]) => [key, toEncodable(item)]));
41
+ if (value && typeof value === 'object' && !Buffer.isBuffer(value) && !(value instanceof Uint8Array)) {
42
+ const encodable = {};
43
+ for (const [key, item] of Object.entries(value))
44
+ if (item !== undefined && item !== null)
45
+ encodable[key] = toEncodable(item);
46
+ return encodable;
47
+ }
48
+ return value;
49
+ }
@@ -0,0 +1 @@
1
+ export {};