Skip to content

Commit 226f985

Browse files
committed
Allow ValueTypeObject to be provided as a Partial
1 parent ac19c62 commit 226f985

File tree

4 files changed

+244
-35
lines changed

4 files changed

+244
-35
lines changed

src/core/action.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { ActionDescriptor, parseActionDescriptorString, stringifyEventTarget } f
22
import { Token } from "../mutation-observers"
33
import { Schema } from "./schema"
44
import { camelize } from "./string_helpers"
5+
import { hasProperty } from "./utils"
6+
57
export class Action {
68
readonly element: Element
79
readonly index: number
@@ -54,7 +56,7 @@ export class Action {
5456
return false
5557
}
5658

57-
if (!Object.prototype.hasOwnProperty.call(this.keyMappings, standardFilter)) {
59+
if (!hasProperty(this.keyMappings, standardFilter)) {
5860
error(`contains unknown key filter: ${this.keyFilter}`)
5961
}
6062

src/core/utils.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export function isSomething(object: any): boolean {
2+
return object !== null && object !== undefined
3+
}
4+
5+
export function hasProperty(object: any, property: string): boolean {
6+
return Object.prototype.hasOwnProperty.call(object, property)
7+
}

src/core/value_properties.ts

Lines changed: 59 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Constructor } from "./constructor"
22
import { Controller } from "./controller"
33
import { readInheritableStaticObjectPairs } from "./inheritable_statics"
44
import { camelize, capitalize, dasherize } from "./string_helpers"
5+
import { isSomething, hasProperty } from "./utils"
56

67
export function ValuePropertiesBlessing<T>(constructor: Constructor<T>) {
78
const valueDefinitionPairs = readInheritableStaticObjectPairs<T, ValueTypeDefinition>(constructor, "values")
@@ -77,7 +78,7 @@ export type ValueTypeConstant = typeof Array | typeof Boolean | typeof Number |
7778

7879
export type ValueTypeDefault = Array<any> | boolean | number | Object | string
7980

80-
export type ValueTypeObject = { type: ValueTypeConstant; default: ValueTypeDefault }
81+
export type ValueTypeObject = Partial<{ type: ValueTypeConstant; default: ValueTypeDefault }>
8182

8283
export type ValueTypeDefinition = ValueTypeConstant | ValueTypeDefault | ValueTypeObject
8384

@@ -91,7 +92,7 @@ function parseValueDefinitionPair([token, typeDefinition]: ValueDefinitionPair,
9192
})
9293
}
9394

94-
function parseValueTypeConstant(constant: ValueTypeConstant) {
95+
export function parseValueTypeConstant(constant?: ValueTypeConstant) {
9596
switch (constant) {
9697
case Array:
9798
return "array"
@@ -106,7 +107,7 @@ function parseValueTypeConstant(constant: ValueTypeConstant) {
106107
}
107108
}
108109

109-
function parseValueTypeDefault(defaultValue: ValueTypeDefault) {
110+
export function parseValueTypeDefault(defaultValue?: ValueTypeDefault) {
110111
switch (typeof defaultValue) {
111112
case "boolean":
112113
return "boolean"
@@ -120,73 +121,97 @@ function parseValueTypeDefault(defaultValue: ValueTypeDefault) {
120121
if (Object.prototype.toString.call(defaultValue) === "[object Object]") return "object"
121122
}
122123

123-
function parseValueTypeObject(payload: { controller?: string; token: string; typeObject: ValueTypeObject }) {
124-
const typeFromObject = parseValueTypeConstant(payload.typeObject.type)
124+
type ValueTypeObjectPayload = {
125+
controller?: string
126+
token: string
127+
typeObject: ValueTypeObject
128+
}
129+
130+
export function parseValueTypeObject(payload: ValueTypeObjectPayload) {
131+
const { controller, token, typeObject } = payload
132+
133+
const hasType = isSomething(typeObject.type)
134+
const hasDefault = isSomething(typeObject.default)
125135

126-
if (!typeFromObject) return
136+
const fullObject = hasType && hasDefault
137+
const onlyType = hasType && !hasDefault
138+
const onlyDefault = !hasType && hasDefault
127139

128-
const defaultValueType = parseValueTypeDefault(payload.typeObject.default)
140+
const typeFromObject = parseValueTypeConstant(typeObject.type)
141+
const typeFromDefaultValue = parseValueTypeDefault(payload.typeObject.default)
129142

130-
if (typeFromObject !== defaultValueType) {
131-
const propertyPath = payload.controller ? `${payload.controller}.${payload.token}` : payload.token
143+
if (onlyType) return typeFromObject
144+
if (onlyDefault) return typeFromDefaultValue
145+
146+
if (typeFromObject !== typeFromDefaultValue) {
147+
const propertyPath = controller ? `${controller}.${token}` : token
132148

133149
throw new Error(
134-
`The specified default value for the Stimulus Value "${propertyPath}" must match the defined type "${typeFromObject}". The provided default value of "${payload.typeObject.default}" is of type "${defaultValueType}".`
150+
`The specified default value for the Stimulus Value "${propertyPath}" must match the defined type "${typeFromObject}". The provided default value of "${typeObject.default}" is of type "${typeFromDefaultValue}".`
135151
)
136152
}
137153

138-
return typeFromObject
154+
if (fullObject) return typeFromObject
139155
}
140156

141-
function parseValueTypeDefinition(payload: {
157+
type ValueTypeDefinitionPayload = {
142158
controller?: string
143159
token: string
144160
typeDefinition: ValueTypeDefinition
145-
}): ValueType {
146-
const typeFromObject = parseValueTypeObject({
147-
controller: payload.controller,
148-
token: payload.token,
149-
typeObject: payload.typeDefinition as ValueTypeObject,
150-
})
151-
const typeFromDefaultValue = parseValueTypeDefault(payload.typeDefinition as ValueTypeDefault)
152-
const typeFromConstant = parseValueTypeConstant(payload.typeDefinition as ValueTypeConstant)
161+
}
162+
163+
export function parseValueTypeDefinition(payload: ValueTypeDefinitionPayload): ValueType {
164+
const { controller, token, typeDefinition } = payload
165+
166+
const typeObject = { controller, token, typeObject: typeDefinition as ValueTypeObject }
167+
168+
const typeFromObject = parseValueTypeObject(typeObject as ValueTypeObjectPayload)
169+
const typeFromDefaultValue = parseValueTypeDefault(typeDefinition as ValueTypeDefault)
170+
const typeFromConstant = parseValueTypeConstant(typeDefinition as ValueTypeConstant)
153171

154172
const type = typeFromObject || typeFromDefaultValue || typeFromConstant
155173

156174
if (type) return type
157175

158-
const propertyPath = payload.controller ? `${payload.controller}.${payload.typeDefinition}` : payload.token
176+
const propertyPath = controller ? `${controller}.${typeDefinition}` : token
159177

160-
throw new Error(`Unknown value type "${propertyPath}" for "${payload.token}" value`)
178+
throw new Error(`Unknown value type "${propertyPath}" for "${token}" value`)
161179
}
162180

163-
function defaultValueForDefinition(typeDefinition: ValueTypeDefinition): ValueTypeDefault {
181+
export function defaultValueForDefinition(typeDefinition: ValueTypeDefinition): ValueTypeDefault {
164182
const constant = parseValueTypeConstant(typeDefinition as ValueTypeConstant)
165-
166183
if (constant) return defaultValuesByType[constant]
167184

168-
const defaultValue = (typeDefinition as ValueTypeObject).default
169-
if (defaultValue !== undefined) return defaultValue
185+
const hasDefault = hasProperty(typeDefinition, "default")
186+
const hasType = hasProperty(typeDefinition, "type")
187+
const typeObject = typeDefinition as ValueTypeObject
188+
189+
if (hasDefault) return typeObject.default!
190+
191+
if (hasType) {
192+
const { type } = typeObject
193+
const constantFromType = parseValueTypeConstant(type)
194+
195+
if (constantFromType) return defaultValuesByType[constantFromType]
196+
}
170197

171198
return typeDefinition
172199
}
173200

174-
function valueDescriptorForTokenAndTypeDefinition(payload: {
175-
token: string
176-
typeDefinition: ValueTypeDefinition
177-
controller?: string
178-
}) {
179-
const key = `${dasherize(payload.token)}-value`
201+
function valueDescriptorForTokenAndTypeDefinition(payload: ValueTypeDefinitionPayload) {
202+
const { token, typeDefinition } = payload
203+
204+
const key = `${dasherize(token)}-value`
180205
const type = parseValueTypeDefinition(payload)
181206
return {
182207
type,
183208
key,
184209
name: camelize(key),
185210
get defaultValue() {
186-
return defaultValueForDefinition(payload.typeDefinition)
211+
return defaultValueForDefinition(typeDefinition)
187212
},
188213
get hasCustomDefaultValue() {
189-
return parseValueTypeDefault(payload.typeDefinition) !== undefined
214+
return parseValueTypeDefault(typeDefinition) !== undefined
190215
},
191216
reader: readers[type],
192217
writer: writers[type] || writers.default,
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import { ValueController } from "../../controllers/value_controller"
2+
import { ControllerTestCase } from "../../cases/controller_test_case"
3+
4+
import {
5+
parseValueTypeDefault,
6+
parseValueTypeConstant,
7+
parseValueTypeObject,
8+
parseValueTypeDefinition,
9+
defaultValueForDefinition,
10+
} from "../../../core/value_properties"
11+
12+
export default class ValuePropertiesTests extends ControllerTestCase(ValueController) {
13+
"test parseValueTypeConstant"() {
14+
this.assert.equal(parseValueTypeConstant(String), "string")
15+
this.assert.equal(parseValueTypeConstant(Boolean), "boolean")
16+
this.assert.equal(parseValueTypeConstant(Array), "array")
17+
this.assert.equal(parseValueTypeConstant(Object), "object")
18+
this.assert.equal(parseValueTypeConstant(Number), "number")
19+
20+
this.assert.equal(parseValueTypeConstant("" as any), undefined)
21+
this.assert.equal(parseValueTypeConstant({} as any), undefined)
22+
this.assert.equal(parseValueTypeConstant([] as any), undefined)
23+
this.assert.equal(parseValueTypeConstant(true as any), undefined)
24+
this.assert.equal(parseValueTypeConstant(false as any), undefined)
25+
this.assert.equal(parseValueTypeConstant(0 as any), undefined)
26+
this.assert.equal(parseValueTypeConstant(1 as any), undefined)
27+
this.assert.equal(parseValueTypeConstant(null!), undefined)
28+
this.assert.equal(parseValueTypeConstant(undefined), undefined)
29+
}
30+
31+
"test parseValueTypeDefault"() {
32+
this.assert.equal(parseValueTypeDefault(""), "string")
33+
this.assert.equal(parseValueTypeDefault("Some string"), "string")
34+
35+
this.assert.equal(parseValueTypeDefault(true), "boolean")
36+
this.assert.equal(parseValueTypeDefault(false), "boolean")
37+
38+
this.assert.equal(parseValueTypeDefault([]), "array")
39+
this.assert.equal(parseValueTypeDefault([1, 2, 3]), "array")
40+
this.assert.equal(parseValueTypeDefault([true, false, true]), "array")
41+
this.assert.equal(parseValueTypeDefault([{}, {}, {}]), "array")
42+
43+
this.assert.equal(parseValueTypeDefault({}), "object")
44+
this.assert.equal(parseValueTypeDefault({ one: "key" }), "object")
45+
46+
this.assert.equal(parseValueTypeDefault(-1), "number")
47+
this.assert.equal(parseValueTypeDefault(0), "number")
48+
this.assert.equal(parseValueTypeDefault(1), "number")
49+
this.assert.equal(parseValueTypeDefault(-0.1), "number")
50+
this.assert.equal(parseValueTypeDefault(0.0), "number")
51+
this.assert.equal(parseValueTypeDefault(0.1), "number")
52+
53+
this.assert.equal(parseValueTypeDefault(null!), undefined)
54+
this.assert.equal(parseValueTypeDefault(undefined!), undefined)
55+
}
56+
57+
"test parseValueTypeObject"() {
58+
const typeObject = (typeObject: any) => {
59+
return parseValueTypeObject({ controller: "foo", token: this.controller.identifier, typeObject })
60+
}
61+
62+
this.assert.equal(typeObject({ type: String, default: "" }), "string")
63+
this.assert.equal(typeObject({ type: String, default: "123" }), "string")
64+
this.assert.equal(typeObject({ type: String }), "string")
65+
this.assert.equal(typeObject({ default: "" }), "string")
66+
this.assert.equal(typeObject({ default: "123" }), "string")
67+
68+
this.assert.equal(typeObject({ type: Number, default: 0 }), "number")
69+
this.assert.equal(typeObject({ type: Number, default: 1 }), "number")
70+
this.assert.equal(typeObject({ type: Number, default: -1 }), "number")
71+
this.assert.equal(typeObject({ type: Number }), "number")
72+
this.assert.equal(typeObject({ default: 0 }), "number")
73+
this.assert.equal(typeObject({ default: 1 }), "number")
74+
this.assert.equal(typeObject({ default: -1 }), "number")
75+
76+
this.assert.equal(typeObject({ type: Array, default: [] }), "array")
77+
this.assert.equal(typeObject({ type: Array, default: [1] }), "array")
78+
this.assert.equal(typeObject({ type: Array }), "array")
79+
this.assert.equal(typeObject({ default: [] }), "array")
80+
this.assert.equal(typeObject({ default: [1] }), "array")
81+
82+
this.assert.equal(typeObject({ type: Object, default: {} }), "object")
83+
this.assert.equal(typeObject({ type: Object, default: { some: "key" } }), "object")
84+
this.assert.equal(typeObject({ type: Object }), "object")
85+
this.assert.equal(typeObject({ default: {} }), "object")
86+
this.assert.equal(typeObject({ default: { some: "key" } }), "object")
87+
88+
this.assert.equal(typeObject({ type: Boolean, default: true }), "boolean")
89+
this.assert.equal(typeObject({ type: Boolean, default: false }), "boolean")
90+
this.assert.equal(typeObject({ type: Boolean }), "boolean")
91+
this.assert.equal(typeObject({ default: false }), "boolean")
92+
93+
this.assert.throws(() => typeObject({ type: Boolean, default: "something else" }), {
94+
name: "Error",
95+
message: `The specified default value for the Stimulus Value "foo.test" must match the defined type "boolean". The provided default value of "something else" is of type "string".`,
96+
})
97+
98+
this.assert.throws(() => typeObject({ type: Boolean, default: "true" }), {
99+
name: "Error",
100+
message: `The specified default value for the Stimulus Value "foo.test" must match the defined type "boolean". The provided default value of "true" is of type "string".`,
101+
})
102+
}
103+
104+
"test parseValueTypeDefinition booleans"() {
105+
const typeDefinition = (typeDefinition: any) => {
106+
return parseValueTypeDefinition({ controller: "foo", token: this.controller.identifier, typeDefinition })
107+
}
108+
109+
this.assert.equal(typeDefinition(Boolean), "boolean")
110+
this.assert.equal(typeDefinition(true), "boolean")
111+
this.assert.equal(typeDefinition(false), "boolean")
112+
this.assert.equal(typeDefinition({ type: Boolean, default: false }), "boolean")
113+
this.assert.equal(typeDefinition({ type: Boolean }), "boolean")
114+
this.assert.equal(typeDefinition({ default: true }), "boolean")
115+
116+
// since the provided value is actually an object, it's going to be of type "object"
117+
this.assert.equal(typeDefinition({ default: null }), "object")
118+
this.assert.equal(typeDefinition({ default: undefined }), "object")
119+
120+
this.assert.equal(typeDefinition({}), "object")
121+
this.assert.equal(typeDefinition(""), "string")
122+
this.assert.equal(typeDefinition([]), "array")
123+
124+
this.assert.throws(() => typeDefinition(null), {
125+
name: "TypeError",
126+
message: `Cannot read properties of null (reading 'type')`,
127+
})
128+
129+
this.assert.throws(() => typeDefinition(undefined), {
130+
name: "TypeError",
131+
message: `Cannot read properties of undefined (reading 'type')`,
132+
})
133+
}
134+
135+
"test defaultValueForDefinition"() {
136+
this.assert.deepEqual(defaultValueForDefinition(String), "")
137+
this.assert.deepEqual(defaultValueForDefinition(Boolean), false)
138+
this.assert.deepEqual(defaultValueForDefinition(Object), {})
139+
this.assert.deepEqual(defaultValueForDefinition(Array), [])
140+
this.assert.deepEqual(defaultValueForDefinition(Number), 0)
141+
142+
this.assert.deepEqual(defaultValueForDefinition({ type: String }), "")
143+
this.assert.deepEqual(defaultValueForDefinition({ type: Boolean }), false)
144+
this.assert.deepEqual(defaultValueForDefinition({ type: Object }), {})
145+
this.assert.deepEqual(defaultValueForDefinition({ type: Array }), [])
146+
this.assert.deepEqual(defaultValueForDefinition({ type: Number }), 0)
147+
148+
this.assert.deepEqual(defaultValueForDefinition({ type: String, default: null }), null)
149+
this.assert.deepEqual(defaultValueForDefinition({ type: Boolean, default: null }), null)
150+
this.assert.deepEqual(defaultValueForDefinition({ type: Object, default: null }), null)
151+
this.assert.deepEqual(defaultValueForDefinition({ type: Array, default: null }), null)
152+
this.assert.deepEqual(defaultValueForDefinition({ type: Number, default: null }), null)
153+
154+
this.assert.deepEqual(defaultValueForDefinition({ type: String, default: "some string" }), "some string")
155+
this.assert.deepEqual(defaultValueForDefinition({ type: Boolean, default: true }), true)
156+
this.assert.deepEqual(defaultValueForDefinition({ type: Object, default: { some: "key" } }), { some: "key" })
157+
this.assert.deepEqual(defaultValueForDefinition({ type: Array, default: [1, 2, 3] }), [1, 2, 3])
158+
this.assert.deepEqual(defaultValueForDefinition({ type: Number, default: 99 }), 99)
159+
160+
this.assert.deepEqual(defaultValueForDefinition("some string"), "some string")
161+
this.assert.deepEqual(defaultValueForDefinition(true), true)
162+
this.assert.deepEqual(defaultValueForDefinition({ some: "key" }), { some: "key" })
163+
this.assert.deepEqual(defaultValueForDefinition([1, 2, 3]), [1, 2, 3])
164+
this.assert.deepEqual(defaultValueForDefinition(99), 99)
165+
166+
this.assert.deepEqual(defaultValueForDefinition({ default: "some string" }), "some string")
167+
this.assert.deepEqual(defaultValueForDefinition({ default: true }), true)
168+
this.assert.deepEqual(defaultValueForDefinition({ default: { some: "key" } }), { some: "key" })
169+
this.assert.deepEqual(defaultValueForDefinition({ default: [1, 2, 3] }), [1, 2, 3])
170+
this.assert.deepEqual(defaultValueForDefinition({ default: 99 }), 99)
171+
172+
this.assert.deepEqual(defaultValueForDefinition({ default: null }), null)
173+
this.assert.deepEqual(defaultValueForDefinition({ default: undefined }), undefined)
174+
}
175+
}

0 commit comments

Comments
 (0)