@nan0web/ui 1.0.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 (131) hide show
  1. package/.datasets/README.dataset.jsonl +12 -0
  2. package/.editorconfig +20 -0
  3. package/CONTRIBUTING.md +42 -0
  4. package/LICENSE +15 -0
  5. package/README.md +238 -0
  6. package/docs/uk/README.md +240 -0
  7. package/package.json +64 -0
  8. package/playground/User.js +52 -0
  9. package/playground/currency.exchange.js +48 -0
  10. package/playground/i18n/index.js +21 -0
  11. package/playground/i18n/uk.js +53 -0
  12. package/playground/language.form.js +25 -0
  13. package/playground/main.js +72 -0
  14. package/playground/registration.form.js +58 -0
  15. package/playground/topup.telephone.js +62 -0
  16. package/src/App/Command/Options.js +78 -0
  17. package/src/App/Command/index.js +9 -0
  18. package/src/App/Core/CoreApp.js +129 -0
  19. package/src/App/Core/UI.js +116 -0
  20. package/src/App/Core/Widget.js +67 -0
  21. package/src/App/Core/index.js +11 -0
  22. package/src/App/Scenario.js +45 -0
  23. package/src/App/User/Command/Message.js +44 -0
  24. package/src/App/User/Command/Options.js +48 -0
  25. package/src/App/User/Command/index.js +11 -0
  26. package/src/App/User/UserApp.js +73 -0
  27. package/src/App/User/UserApp.test.js +56 -0
  28. package/src/App/User/UserUI.js +20 -0
  29. package/src/App/User/UserUI.test.js +51 -0
  30. package/src/App/User/index.js +15 -0
  31. package/src/App/index.js +22 -0
  32. package/src/Component/Process/Input.js +70 -0
  33. package/src/Component/Process/Process.js +26 -0
  34. package/src/Component/Process/index.js +5 -0
  35. package/src/Component/Welcome/Input.js +50 -0
  36. package/src/Component/Welcome/Welcome.js +26 -0
  37. package/src/Component/Welcome/index.js +5 -0
  38. package/src/Component/index.js +9 -0
  39. package/src/Frame/Frame.js +591 -0
  40. package/src/Frame/Frame.test.js +429 -0
  41. package/src/Frame/Props.js +102 -0
  42. package/src/Locale.js +119 -0
  43. package/src/Model/User/User.js +56 -0
  44. package/src/Model/index.js +7 -0
  45. package/src/README.md.js +371 -0
  46. package/src/StdIn.js +111 -0
  47. package/src/StdOut.js +99 -0
  48. package/src/View/RenderOptions.js +48 -0
  49. package/src/View/View.js +289 -0
  50. package/src/View/View.test.js +77 -0
  51. package/src/core/Form/Form.js +289 -0
  52. package/src/core/Form/Form.test.js +116 -0
  53. package/src/core/Form/Input.js +116 -0
  54. package/src/core/Form/Input.test.js +58 -0
  55. package/src/core/Form/Message.js +86 -0
  56. package/src/core/Form/Message.test.js +54 -0
  57. package/src/core/Form/index.js +11 -0
  58. package/src/core/InputAdapter.js +41 -0
  59. package/src/core/InputAdapter.test.js +35 -0
  60. package/src/core/Message/InputMessage.js +119 -0
  61. package/src/core/Message/InputMessage.test.js +45 -0
  62. package/src/core/Message/Message.js +77 -0
  63. package/src/core/Message/Message.test.js +58 -0
  64. package/src/core/Message/OutputMessage.js +143 -0
  65. package/src/core/Message/OutputMessage.test.js +61 -0
  66. package/src/core/Message/index.js +7 -0
  67. package/src/core/OutputAdapter.js +50 -0
  68. package/src/core/OutputAdapter.test.js +35 -0
  69. package/src/core/Stream.js +71 -0
  70. package/src/core/Stream.test.js +78 -0
  71. package/src/core/StreamEntry.js +59 -0
  72. package/src/core/index.js +13 -0
  73. package/src/functions.js +38 -0
  74. package/src/index.js +34 -0
  75. package/src/index.test.js +14 -0
  76. package/src/models/SimpleUser.js +18 -0
  77. package/stories/App/AppView.js +15 -0
  78. package/stories/App/AppView.test.js +22 -0
  79. package/stories/App/RenderOptions.js +14 -0
  80. package/stories/nodejs/interface.test.js +27 -0
  81. package/system.md +187 -0
  82. package/system1.md +137 -0
  83. package/task.md +181 -0
  84. package/tsconfig.json +23 -0
  85. package/types/App/Command/Options.d.ts +46 -0
  86. package/types/App/Command/index.d.ts +8 -0
  87. package/types/App/Core/CoreApp.d.ts +70 -0
  88. package/types/App/Core/UI.d.ts +49 -0
  89. package/types/App/Core/Widget.d.ts +40 -0
  90. package/types/App/Core/index.d.ts +10 -0
  91. package/types/App/Scenario.d.ts +26 -0
  92. package/types/App/User/Command/Message.d.ts +30 -0
  93. package/types/App/User/Command/Options.d.ts +27 -0
  94. package/types/App/User/Command/index.d.ts +8 -0
  95. package/types/App/User/UserApp.d.ts +31 -0
  96. package/types/App/User/UserUI.d.ts +18 -0
  97. package/types/App/User/index.d.ts +12 -0
  98. package/types/App/index.d.ts +14 -0
  99. package/types/Component/Process/Input.d.ts +48 -0
  100. package/types/Component/Process/Process.d.ts +13 -0
  101. package/types/Component/Process/index.d.ts +4 -0
  102. package/types/Component/Welcome/Input.d.ts +34 -0
  103. package/types/Component/Welcome/Welcome.d.ts +13 -0
  104. package/types/Component/Welcome/index.d.ts +4 -0
  105. package/types/Component/index.d.ts +8 -0
  106. package/types/Frame/Frame.d.ts +186 -0
  107. package/types/Frame/Props.d.ts +77 -0
  108. package/types/Locale.d.ts +55 -0
  109. package/types/Model/User/User.d.ts +36 -0
  110. package/types/Model/index.d.ts +6 -0
  111. package/types/StdIn.d.ts +62 -0
  112. package/types/StdOut.d.ts +52 -0
  113. package/types/View/RenderOptions.d.ts +29 -0
  114. package/types/View/View.d.ts +115 -0
  115. package/types/core/Form/Form.d.ts +123 -0
  116. package/types/core/Form/Input.d.ts +69 -0
  117. package/types/core/Form/Message.d.ts +28 -0
  118. package/types/core/Form/index.d.ts +5 -0
  119. package/types/core/InputAdapter.d.ts +28 -0
  120. package/types/core/Message/InputMessage.d.ts +71 -0
  121. package/types/core/Message/Message.d.ts +50 -0
  122. package/types/core/Message/OutputMessage.d.ts +53 -0
  123. package/types/core/Message/index.d.ts +5 -0
  124. package/types/core/OutputAdapter.d.ts +33 -0
  125. package/types/core/Stream.d.ts +27 -0
  126. package/types/core/StreamEntry.d.ts +45 -0
  127. package/types/core/index.d.ts +9 -0
  128. package/types/functions.d.ts +3 -0
  129. package/types/index.d.ts +20 -0
  130. package/types/models/SimpleUser.d.ts +21 -0
  131. package/vitest.config.js +26 -0
@@ -0,0 +1,116 @@
1
+ import { describe, it } from "node:test"
2
+ import { strict as assert } from "node:assert"
3
+ import UIForm from "./Form.js"
4
+ import FormInput from "./Input.js"
5
+
6
+ describe("UIForm", () => {
7
+ it("should create instance with default values", () => {
8
+ const form = new UIForm()
9
+ assert.ok(form instanceof UIForm)
10
+ assert.equal(form.title, "")
11
+ assert.deepEqual(form.fields, [])
12
+ assert.deepEqual(form.state, {})
13
+ })
14
+
15
+ it("should create instance with custom values", () => {
16
+ const fields = [
17
+ new FormInput({ name: "name", label: "Name", required: true }),
18
+ new FormInput({ name: "email", label: "Email", type: "email" })
19
+ ]
20
+ const props = {
21
+ title: "Test Form",
22
+ fields,
23
+ state: { name: "John" }
24
+ }
25
+ const form = new UIForm(props)
26
+ assert.equal(form.title, "Test Form")
27
+ assert.equal(form.fields.length, 2)
28
+ assert.deepEqual(form.state, { name: "John" })
29
+ })
30
+
31
+ it("should set data", () => {
32
+ const form = new UIForm({ state: { name: "John" } })
33
+ const newForm = form.setData({ email: "john@example.com" })
34
+ assert.deepEqual(newForm.state, { name: "John", email: "john@example.com" })
35
+ })
36
+
37
+ it("should get field by name", () => {
38
+ const fields = [new FormInput({ name: "test", label: "Test Field" })]
39
+ const form = new UIForm({ fields })
40
+ const field = form.getField("test")
41
+ assert.ok(field)
42
+ assert.equal(field.name, "test")
43
+ })
44
+
45
+ it("should get values", () => {
46
+ const form = new UIForm({ state: { name: "John", age: 30 } })
47
+ const values = form.getValues()
48
+ assert.deepEqual(values, { name: "John", age: 30 })
49
+ })
50
+
51
+ it("should validate form", () => {
52
+ const fields = [new FormInput({ name: "requiredField", required: true })]
53
+ const form = new UIForm({ fields, state: {} })
54
+ const result = form.validate()
55
+ assert.ok(!result.isValid)
56
+ assert.ok(result.errors.requiredField)
57
+ })
58
+
59
+ it("should validate individual field", () => {
60
+ const fields = [new FormInput({ name: "email", type: "email" })]
61
+ const form = new UIForm({ fields })
62
+ const result = form.validateField("email", "invalid-email")
63
+ assert.ok(!result.isValid)
64
+ assert.ok(result.errors.email)
65
+ })
66
+
67
+ it("should convert to JSON", () => {
68
+ const form = new UIForm({ title: "Test", state: { name: "John" } })
69
+ const json = form.toJSON()
70
+ assert.ok(json.id)
71
+ assert.equal(json.title, "Test")
72
+ assert.ok(Array.isArray(json.fields))
73
+ assert.ok(json.state)
74
+ assert.ok(json.meta)
75
+ })
76
+
77
+ it("parses default text fields", () => {
78
+ const data = { name: "", email: "" }
79
+ const { fields } = UIForm.parse(data)
80
+ assert.equal(fields.length, 2)
81
+ assert.ok(fields[0] instanceof FormInput)
82
+ assert.equal(fields[0].name, "name")
83
+ assert.equal(fields[0].label, "Name")
84
+ assert.equal(fields[0].type, FormInput.TYPES.TEXT)
85
+ assert.equal(fields[0].required, false)
86
+ })
87
+
88
+ it("applies overrides correctly", () => {
89
+ const data = { age: "" }
90
+ const { fields } = UIForm.parse(data, {
91
+ age: { type: FormInput.TYPES.NUMBER, required: true, label: "User Age" },
92
+ })
93
+ const ageField = fields[0]
94
+ assert.equal(ageField.type, FormInput.TYPES.NUMBER)
95
+ assert.ok(ageField.required)
96
+ assert.equal(ageField.label, "User Age")
97
+ })
98
+
99
+ it("supports static custom validators", () => {
100
+ // register a validator that ensures a string contains only digits
101
+ UIForm.addValidator("digitsOnly", value => {
102
+ return /^\d+$/.test(value) ? true : "Must contain only digits"
103
+ })
104
+
105
+ const fields = [
106
+ new FormInput({ name: "phone", label: "Phone", required: true })
107
+ ]
108
+ const schema = {
109
+ phone: { validator: "digitsOnly" }
110
+ }
111
+ const form = new UIForm({ fields, schema, state: { phone: "12a34" } })
112
+ const result = form.validate()
113
+ assert.ok(!result.isValid)
114
+ assert.equal(result.errors.phone, "Must contain only digits")
115
+ })
116
+ })
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Form input field descriptor.
3
+ *
4
+ * @class FormInput
5
+ * @property {string} name - Field name.
6
+ * @property {string} label - Display label.
7
+ * @property {string} type - Input type (text, email, number, select, etc.).
8
+ * @property {boolean} required - Whether the field is required.
9
+ * @property {string} placeholder - Placeholder text.
10
+ * @property {Array<string>} options - Select options (if type is 'select').
11
+ * @property {Function|null} validator - Custom validation function.
12
+ * @property {*} defaultValue - Default value.
13
+ */
14
+ export default class FormInput {
15
+ /** @type {string} */ name = ''
16
+ /** @type {string} */ label = ''
17
+ /** @type {string} */ type = 'text'
18
+ /** @type {boolean} */ required = false
19
+ /** @type {string} */ placeholder = ''
20
+ /** @type {Array<string>} */ options = []
21
+ /** @type {Function|null} */ validator = null
22
+ /** @type {*} */ defaultValue = null
23
+
24
+ /**
25
+ * Predefined input types.
26
+ */
27
+ static TYPES = {
28
+ TEXT: 'text',
29
+ EMAIL: 'email',
30
+ NUMBER: 'number',
31
+ SELECT: 'select',
32
+ CHECKBOX: 'checkbox',
33
+ TEXTAREA: 'textarea'
34
+ }
35
+
36
+ /**
37
+ * Create a new form input.
38
+ *
39
+ * @param {Object} props - Input properties.
40
+ * @param {string} props.name - Field name.
41
+ * @param {string} [props.label=props.name] - Display label.
42
+ * @param {string} [props.type='text'] - Input type.
43
+ * @param {boolean} [props.required=false] - Is required.
44
+ * @param {string} [props.placeholder=''] - Placeholder.
45
+ * @param {Array<string>} [props.options=[]] - Select options.
46
+ * @param {Function} [props.validator=null] - Custom validator.
47
+ * @param {*} [props.defaultValue=null] - Default value.
48
+ */
49
+ constructor(props) {
50
+ const {
51
+ name,
52
+ label = name,
53
+ type = FormInput.TYPES.TEXT,
54
+ required = this.required,
55
+ placeholder = this.placeholder,
56
+ options = [],
57
+ validator = this.validator,
58
+ defaultValue = this.defaultValue
59
+ } = props
60
+
61
+ if (!name) {
62
+ throw new TypeError('FormInput.name is required')
63
+ }
64
+
65
+ this.name = String(name)
66
+ this.label = String(label)
67
+ this.type = String(type)
68
+ this.required = Boolean(required)
69
+ this.placeholder = String(placeholder)
70
+ this.options = options
71
+ this.validator = validator
72
+ this.defaultValue = defaultValue
73
+
74
+ this.requireValidType()
75
+ }
76
+
77
+ requireValidType() {
78
+ if (!Object.values(FormInput.TYPES).includes(this.type)) {
79
+ throw new TypeError([
80
+ "FormInput.type is invalid!",
81
+ ["Provided", this.type].join(": "),
82
+ "Available types:",
83
+ ...Object.values(FormInput.TYPES).map(t => ` - ${t}`)
84
+ ].join("\n"))
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Serialises the input to a plain JSON object.
90
+ *
91
+ * @returns {Object}
92
+ */
93
+ toJSON() {
94
+ return {
95
+ name: this.name,
96
+ label: this.label,
97
+ type: this.type,
98
+ required: this.required,
99
+ placeholder: this.placeholder,
100
+ options: this.options,
101
+ defaultValue: this.defaultValue
102
+ }
103
+ }
104
+
105
+ /**
106
+ * @param {*} input
107
+ * @returns {FormInput}
108
+ */
109
+ static from(input) {
110
+ if (input instanceof FormInput) return input
111
+ if ("string" === typeof input) {
112
+ return new FormInput({ name: input, label: input })
113
+ }
114
+ return new FormInput(input)
115
+ }
116
+ }
@@ -0,0 +1,58 @@
1
+ import { describe, it } from "node:test"
2
+ import { strict as assert } from "node:assert"
3
+ import FormInput from "./Input.js"
4
+
5
+ describe("FormInput", () => {
6
+ it("should create instance with default values", () => {
7
+ const input = new FormInput({ name: "name" })
8
+ assert.ok(input instanceof FormInput)
9
+ assert.equal(input.type, FormInput.TYPES.TEXT)
10
+ assert.equal(input.required, false)
11
+ assert.equal(input.name, "name")
12
+ assert.equal(input.placeholder, "")
13
+ assert.deepEqual(input.options, [])
14
+ })
15
+
16
+ it("should create instance with custom values", () => {
17
+ const props = {
18
+ type: "email",
19
+ name: "email",
20
+ label: "Email Address",
21
+ required: true,
22
+ placeholder: "Enter email",
23
+ options: ["option1", "option2"],
24
+ defaultValue: "test@example.com"
25
+ }
26
+ const input = new FormInput(props)
27
+ assert.equal(input.type, "email")
28
+ assert.equal(input.name, "email")
29
+ assert.equal(input.label, "Email Address")
30
+ assert.equal(input.required, true)
31
+ assert.equal(input.placeholder, "Enter email")
32
+ assert.deepEqual(input.options, ["option1", "option2"])
33
+ assert.equal(input.defaultValue, "test@example.com")
34
+ })
35
+
36
+ it("should create from string", () => {
37
+ const input = FormInput.from("testField")
38
+ assert.equal(input.name, "testField")
39
+ assert.equal(input.label, "testField")
40
+ })
41
+
42
+ it("should create from object", () => {
43
+ const objInput = { name: "test", type: "text" }
44
+ const input = FormInput.from(objInput)
45
+ assert.ok(input instanceof FormInput)
46
+ assert.equal(input.name, "test")
47
+ })
48
+
49
+ it("should validate type", async () => {
50
+ const validInput = new FormInput({ name: "email", type: "email" })
51
+ assert.ok(validInput)
52
+ const fn = async () => new FormInput({ name: "email", type: "invalid" })
53
+ await assert.rejects(fn, {
54
+ name: "TypeError",
55
+ message: /FormInput\.type is invalid!/
56
+ })
57
+ })
58
+ })
@@ -0,0 +1,86 @@
1
+ import OutputMessage from "../Message/OutputMessage.js"
2
+
3
+ /**
4
+ * FormMessage – specialized OutputMessage for forms.
5
+ *
6
+ * @class FormMessage
7
+ * @extends OutputMessage
8
+ */
9
+ export default class FormMessage extends OutputMessage {
10
+ /**
11
+ * Creates a FormMessage.
12
+ *
13
+ * @param {Object} [input={}] - Message properties.
14
+ */
15
+ constructor(input = {}) {
16
+ super({
17
+ ...input,
18
+ type: OutputMessage.TYPES.FORM,
19
+ })
20
+ const {
21
+ data = {},
22
+ schema = {},
23
+ } = input
24
+
25
+ // Store data and schema for easy access
26
+ this.data = data
27
+ this.schema = schema
28
+ }
29
+
30
+ /**
31
+ * Returns a new FormMessage with merged data.
32
+ *
33
+ * @param {Object} newData - Data to merge.
34
+ * @returns {FormMessage}
35
+ */
36
+ addData(newData) {
37
+ return new FormMessage({
38
+ ...this,
39
+ data: { ...this.data, ...newData },
40
+ })
41
+ }
42
+
43
+ /**
44
+ * Validates the provided data against the schema.
45
+ *
46
+ * @param {Object} data - Data to validate.
47
+ * @returns {{isValid: boolean, errors: Object}}
48
+ */
49
+ validateData(data) {
50
+ const errors = {}
51
+
52
+ if (!this.schema) return { isValid: true, errors }
53
+
54
+ for (const [field, rules] of Object.entries(this.schema)) {
55
+ const value = data[field]
56
+
57
+ if (rules.required && (value === undefined || value === null || value === '')) {
58
+ errors[field] = 'Field is required'
59
+ continue
60
+ }
61
+
62
+ if (rules.type) {
63
+ if (rules.type === 'email') {
64
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
65
+ if (value && !emailRegex.test(value)) {
66
+ errors[field] = 'Invalid email format'
67
+ }
68
+ } else if (rules.type === 'number') {
69
+ if (value !== '' && isNaN(Number(value))) {
70
+ errors[field] = 'Must be a number'
71
+ }
72
+ }
73
+ }
74
+
75
+ if (rules.minLength && value && value.length < rules.minLength) {
76
+ errors[field] = `Minimum length is ${rules.minLength}`
77
+ }
78
+
79
+ if (rules.maxLength && value && value.length > rules.maxLength) {
80
+ errors[field] = `Maximum length is ${rules.maxLength}`
81
+ }
82
+ }
83
+
84
+ return { isValid: Object.keys(errors).length === 0, errors }
85
+ }
86
+ }
@@ -0,0 +1,54 @@
1
+ import { describe, it } from "node:test"
2
+ import { strict as assert } from "node:assert"
3
+ import FormMessage from "./Message.js"
4
+
5
+ describe("FormMessage", () => {
6
+ it("should create instance with default values", () => {
7
+ const msg = new FormMessage()
8
+ assert.ok(msg instanceof FormMessage)
9
+ assert.deepEqual(msg.data, {})
10
+ assert.deepEqual(msg.schema, {})
11
+ assert.equal(msg.type, "form")
12
+ })
13
+
14
+ it("should create instance with custom values", () => {
15
+ const props = {
16
+ data: { name: "John" },
17
+ schema: { name: { required: true } },
18
+ body: ["Form content"]
19
+ }
20
+ const msg = new FormMessage(props)
21
+ assert.deepEqual(msg.data, { name: "John" })
22
+ assert.deepEqual(msg.schema, { name: { required: true } })
23
+ assert.deepEqual(msg.body, ["Form content"])
24
+ })
25
+
26
+ it("should add data", () => {
27
+ const msg = new FormMessage({ data: { name: "John" } })
28
+ const newMsg = msg.addData({ age: 30 })
29
+ assert.deepEqual(newMsg.data, { name: "John", age: 30 })
30
+ })
31
+
32
+ it("should validate data correctly", () => {
33
+ const schema = {
34
+ name: { required: true },
35
+ email: { type: "email" },
36
+ age: { type: "number" }
37
+ }
38
+ const msg = new FormMessage({ schema })
39
+
40
+ // Valid data
41
+ const validData = { name: "John", email: "john@example.com", age: "30" }
42
+ const validResult = msg.validateData(validData)
43
+ assert.ok(validResult.isValid)
44
+ assert.deepEqual(validResult.errors, {})
45
+
46
+ // Invalid data
47
+ const invalidData = { email: "invalid-email", age: "not-a-number" }
48
+ const invalidResult = msg.validateData(invalidData)
49
+ assert.ok(!invalidResult.isValid)
50
+ assert.ok(invalidResult.errors.name)
51
+ assert.ok(invalidResult.errors.email)
52
+ assert.ok(invalidResult.errors.age)
53
+ })
54
+ })
@@ -0,0 +1,11 @@
1
+ import FormInput from "./Input.js"
2
+ import FormMessage from "./Message.js"
3
+ import Form from "./Form.js"
4
+
5
+ export {
6
+ FormInput,
7
+ FormMessage,
8
+ Form,
9
+ }
10
+
11
+ export default Form
@@ -0,0 +1,41 @@
1
+ import Event from "@nan0web/event/oop"
2
+ import InputMessage from "./Message/InputMessage.js"
3
+
4
+ /**
5
+ * Abstract input adapter for UI implementations.
6
+ *
7
+ * @class InputAdapter
8
+ * @extends Event
9
+ */
10
+ class InputAdapter extends Event {
11
+ /**
12
+ * Starts listening for input and emits an `input` event.
13
+ *
14
+ * @returns {void}
15
+ */
16
+ start() {
17
+ this.emit('input',
18
+ InputMessage.from({ value: "Adapter started" })
19
+ )
20
+ }
21
+
22
+ /**
23
+ * Stops listening for input. Default implementation does nothing.
24
+ *
25
+ * @returns {void}
26
+ */
27
+ stop() {
28
+ // Default implementation – does nothing
29
+ }
30
+
31
+ /**
32
+ * Checks whether the adapter is ready to receive input.
33
+ *
34
+ * @returns {boolean} Always true in base class.
35
+ */
36
+ isReady() {
37
+ return true
38
+ }
39
+ }
40
+
41
+ export default InputAdapter
@@ -0,0 +1,35 @@
1
+ import { describe, it } from "node:test"
2
+ import { strict as assert } from "node:assert"
3
+ import InputAdapter from "./InputAdapter.js"
4
+
5
+ describe("InputAdapter", () => {
6
+ it("should create instance", () => {
7
+ const adapter = new InputAdapter()
8
+ assert.ok(adapter instanceof InputAdapter)
9
+ })
10
+
11
+ it("should start and emit input message", () => {
12
+ const adapter = new InputAdapter()
13
+ let emitted = false
14
+ let message = null
15
+
16
+ adapter.on('input', (msg) => {
17
+ emitted = true
18
+ message = msg
19
+ })
20
+
21
+ adapter.start()
22
+ assert.ok(emitted)
23
+ assert.ok(message)
24
+ })
25
+
26
+ it("should stop without error", () => {
27
+ const adapter = new InputAdapter()
28
+ assert.doesNotThrow(() => adapter.stop())
29
+ })
30
+
31
+ it("should be ready by default", () => {
32
+ const adapter = new InputAdapter()
33
+ assert.ok(adapter.isReady())
34
+ })
35
+ })
@@ -0,0 +1,119 @@
1
+ import { notEmpty } from "@nan0web/types"
2
+ import { Message } from "@nan0web/co"
3
+
4
+ /** @typedef {Message | string | null} InputMessageValue */
5
+
6
+ /**
7
+ * Represents a message input with value, options, and metadata.
8
+ */
9
+ export default class InputMessage {
10
+ static ESCAPE = String.fromCharCode(27)
11
+ /** @type {InputMessageValue} Input value */
12
+ value
13
+
14
+ /** @type {string[]} Available options for this input */
15
+ options
16
+
17
+ /** @type {boolean} Whether this input is waiting for response */
18
+ waiting
19
+
20
+ /** @type {number} Timestamp when input was created */
21
+ #time
22
+
23
+ /**
24
+ * Creates a new InputMessage instance.
25
+ * @param {object} props - Input message properties
26
+ * @param {InputMessageValue} [props.value=""] - Input value
27
+ * @param {string[]|string} [props.options=[]] - Available options
28
+ * @param {boolean} [props.waiting=false] - Waiting state flag
29
+ * @param {boolean} [props.escaped=false] - Sets value to escape when true
30
+ */
31
+ constructor(props = {}) {
32
+ if ("string" === typeof props) {
33
+ props = { value: props }
34
+ }
35
+ const {
36
+ value = "",
37
+ waiting = false,
38
+ options = [],
39
+ escaped = false,
40
+ } = props
41
+ this.#time = Date.now()
42
+ this.waiting = Boolean(waiting)
43
+
44
+ // Properly handle string options by converting to array
45
+ this.options = Array.isArray(options) ? options.map(String) : [String(options)]
46
+ this.value = String(value)
47
+ if (!this.value && escaped) {
48
+ this.value = this.ESCAPE
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Checks if the input value is empty.
54
+ * @returns {boolean} True if value is empty or null, false otherwise
55
+ */
56
+ get empty() {
57
+ return null === this.value || 0 === String(this.value).length
58
+ }
59
+
60
+ /**
61
+ * Gets the timestamp when input was created.
62
+ * @returns {number} Creation timestamp
63
+ */
64
+ get time() {
65
+ return this.#time
66
+ }
67
+
68
+ /**
69
+ * Returns the escape value.
70
+ * @returns {string}
71
+ */
72
+ get ESCAPE() {
73
+ return /** @type {typeof InputMessage} */ (this.constructor).ESCAPE
74
+ }
75
+
76
+ /**
77
+ * Checks if the input is an escape sequence.
78
+ * @returns {boolean} True if input value is escape sequence, false otherwise
79
+ */
80
+ get escaped() {
81
+ return this.ESCAPE === this.value
82
+ }
83
+
84
+ /**
85
+ * Validates if the input has a non-empty value.
86
+ * @returns {boolean} True if input is valid, false otherwise
87
+ */
88
+ get isValid() {
89
+ // An input is valid only if it has a non-empty value and is not an escape sequence
90
+ return notEmpty(this.value) && this.value !== this.ESCAPE
91
+ }
92
+
93
+ /**
94
+ * Converts the input to a plain object representation.
95
+ * @returns {object} Object with all properties including timestamp
96
+ */
97
+ toObject() {
98
+ return { ...this, time: this.time }
99
+ }
100
+
101
+ /**
102
+ * Converts the input to a string representation including timestamp.
103
+ * @returns {string} String representation with timestamp and value
104
+ */
105
+ toString() {
106
+ const date = new Date(this.time)
107
+ return `${date.toISOString().split(".")[0]} ${this.value}`
108
+ }
109
+
110
+ /**
111
+ * Creates an InputMessage instance from the given value.
112
+ * @param {InputMessage|object|string} value - The value to create from
113
+ * @returns {InputMessage} An InputMessage instance
114
+ */
115
+ static from(value) {
116
+ if (value instanceof InputMessage) return value
117
+ return new this(value)
118
+ }
119
+ }
@@ -0,0 +1,45 @@
1
+ import { describe, it } from "node:test"
2
+ import { strict as assert } from "node:assert"
3
+ import InputMessage from "./InputMessage.js"
4
+ import { notEmpty, empty } from "@nan0web/types"
5
+
6
+ describe("InputMessage", () => {
7
+ it("should create instance with default values", () => {
8
+ const msg = new InputMessage()
9
+ assert.ok(msg instanceof InputMessage)
10
+ assert.equal(msg.value, "")
11
+ assert.equal(msg.waiting, false)
12
+ assert.deepEqual(msg.options, [])
13
+ })
14
+
15
+ it("should create instance with custom values", () => {
16
+ const props = {
17
+ value: "user input",
18
+ waiting: true,
19
+ options: ["option1", "option2"]
20
+ }
21
+ const msg = new InputMessage(props)
22
+ assert.equal(msg.value, "user input")
23
+ assert.equal(msg.waiting, true)
24
+ assert.deepEqual(msg.options, ["option1", "option2"])
25
+ })
26
+
27
+ it("should handle string options correctly", () => {
28
+ const msg = new InputMessage({ options: "single-option" })
29
+ assert.deepEqual(msg.options, ["single-option"])
30
+ })
31
+
32
+ it("should detect empty value", () => {
33
+ const emptyMsg = new InputMessage({ value: "" })
34
+ const nonEmptyMsg = new InputMessage({ value: "test" })
35
+ assert.ok(emptyMsg.empty)
36
+ assert.ok(!nonEmptyMsg.empty)
37
+ })
38
+
39
+ it("should validate message", () => {
40
+ const validMsg = new InputMessage({ value: "test" })
41
+ const invalidMsg = new InputMessage({ value: "" })
42
+ assert.ok(validMsg.isValid)
43
+ assert.ok(!invalidMsg.isValid)
44
+ })
45
+ })