Skip to content

Commit

Permalink
Improve Stimulus Values type mismatch error messages (#528)
Browse files Browse the repository at this point in the history
* Make types messages much more friendly

* Fix some errors messages

* Add controller name to types mismatch errors

* Print controller in type mismatch error when it possible

* Remove redundant controller presence check

Since the `propertyPath` already has the conditional in it it's 
basically the same logic twice. With that we can just reduce it to the 
second `throw`.

* Reword value type error message

* Improve error message for setting/overriding Stimulus Values

Before:
- "TypeError: Expected object"
- "TypeError: Expected array"

After:
- TypeError: Stimulus Value "my-controller.myObjectValue" -
expected value of type "object" but instead got value "false" of type
"boolean"

- TypeError: Stimulus Value "my-controller.myArrayValue" -
expected value of type "array" but instead got value "123" of type
"number"

Co-authored-by: Marco Roth <marco.roth@intergga.ch>
  • Loading branch information
epszaw and marcoroth committed Jul 16, 2022
1 parent 7f4d0cf commit 5a60cde
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 31 deletions.
19 changes: 13 additions & 6 deletions src/core/value_observer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,21 @@ export class ValueObserver implements StringMapObserverDelegate {

if (typeof changedMethod == "function") {
const descriptor = this.valueDescriptorNameMap[name]
const value = descriptor.reader(rawValue)
let oldValue = rawOldValue

if (rawOldValue) {
oldValue = descriptor.reader(rawOldValue)
}
try {
const value = descriptor.reader(rawValue)
let oldValue = rawOldValue

if (rawOldValue) {
oldValue = descriptor.reader(rawOldValue)
}

changedMethod.call(this.receiver, value, oldValue)
changedMethod.call(this.receiver, value, oldValue)
} catch (error) {
if (!(error instanceof TypeError)) throw error

throw new TypeError(`Stimulus Value "${this.context.identifier}.${descriptor.name}" - ${error.message}`)
}
}
}

Expand Down
63 changes: 38 additions & 25 deletions src/core/value_properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export function ValuePropertiesBlessing<T>(constructor: Constructor<T>) {
valueDescriptorMap: {
get(this: Controller) {
return valueDefinitionPairs.reduce((result, valueDefinitionPair) => {
const valueDescriptor = parseValueDefinitionPair(valueDefinitionPair)
const valueDescriptor = parseValueDefinitionPair(valueDefinitionPair, this.identifier)
const attributeName = this.data.getAttributeNameForKey(valueDescriptor.key)
return Object.assign(result, { [attributeName]: valueDescriptor })
}, {} as ValueDescriptorMap)
Expand All @@ -22,8 +22,8 @@ export function ValuePropertiesBlessing<T>(constructor: Constructor<T>) {
}, propertyDescriptorMap)
}

export function propertiesForValueDefinitionPair<T>(valueDefinitionPair: ValueDefinitionPair): PropertyDescriptorMap {
const definition = parseValueDefinitionPair(valueDefinitionPair)
export function propertiesForValueDefinitionPair<T>(valueDefinitionPair: ValueDefinitionPair, controller?: string): PropertyDescriptorMap {
const definition = parseValueDefinitionPair(valueDefinitionPair, controller)
const { key, name, reader: read, writer: write } = definition

return {
Expand Down Expand Up @@ -80,8 +80,12 @@ export type ValueTypeDefinition = ValueTypeConstant | ValueTypeDefault | ValueTy

export type ValueType = "array" | "boolean" | "number" | "object" | "string"

function parseValueDefinitionPair([token, typeDefinition]: ValueDefinitionPair): ValueDescriptor {
return valueDescriptorForTokenAndTypeDefinition(token, typeDefinition)
function parseValueDefinitionPair([token, typeDefinition]: ValueDefinitionPair, controller?: string): ValueDescriptor {
return valueDescriptorForTokenAndTypeDefinition({
controller,
token,
typeDefinition,
})
}

function parseValueTypeConstant(constant: ValueTypeConstant) {
Expand All @@ -105,29 +109,38 @@ function parseValueTypeDefault(defaultValue: ValueTypeDefault) {
if (Object.prototype.toString.call(defaultValue) === "[object Object]") return "object"
}

function parseValueTypeObject(typeObject: ValueTypeObject) {
const typeFromObject = parseValueTypeConstant(typeObject.type)
function parseValueTypeObject(payload: { controller?: string, token: string, typeObject: ValueTypeObject }) {
const typeFromObject = parseValueTypeConstant(payload.typeObject.type)

if (typeFromObject) {
const defaultValueType = parseValueTypeDefault(typeObject.default)
if (!typeFromObject) return

if (typeFromObject !== defaultValueType) {
throw new Error(`Type "${typeFromObject}" must match the type of the default value. Given default value: "${typeObject.default}" as "${defaultValueType}"`)
}
const defaultValueType = parseValueTypeDefault(payload.typeObject.default)

if (typeFromObject !== defaultValueType) {
const propertyPath = payload.controller ? `${payload.controller}.${payload.token}` : payload.token

return typeFromObject
throw new Error(`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}".`)
}

return typeFromObject
}

function parseValueTypeDefinition(typeDefinition: ValueTypeDefinition): ValueType {
const typeFromObject = parseValueTypeObject(typeDefinition as ValueTypeObject)
const typeFromDefaultValue = parseValueTypeDefault(typeDefinition as ValueTypeDefault)
const typeFromConstant = parseValueTypeConstant(typeDefinition as ValueTypeConstant)
function parseValueTypeDefinition(payload: { controller?: string, token: string, typeDefinition: ValueTypeDefinition }): ValueType {
const typeFromObject = parseValueTypeObject({
controller: payload.controller,
token: payload.token,
typeObject: payload.typeDefinition as ValueTypeObject
})
const typeFromDefaultValue = parseValueTypeDefault(payload.typeDefinition as ValueTypeDefault)
const typeFromConstant = parseValueTypeConstant(payload.typeDefinition as ValueTypeConstant)

const type = typeFromObject || typeFromDefaultValue || typeFromConstant

if (type) return type

throw new Error(`Unknown value type "${typeDefinition}"`)
const propertyPath = payload.controller ? `${payload.controller}.${payload.typeDefinition}` : payload.token

throw new Error(`Unknown value type "${propertyPath}" for "${payload.token}" value`)
}

function defaultValueForDefinition(typeDefinition: ValueTypeDefinition): ValueTypeDefault {
Expand All @@ -141,15 +154,15 @@ function defaultValueForDefinition(typeDefinition: ValueTypeDefinition): ValueTy
return typeDefinition
}

function valueDescriptorForTokenAndTypeDefinition(token: string, typeDefinition: ValueTypeDefinition) {
const key = `${dasherize(token)}-value`
const type = parseValueTypeDefinition(typeDefinition)
function valueDescriptorForTokenAndTypeDefinition(payload: { token: string, typeDefinition: ValueTypeDefinition, controller?: string }) {
const key = `${dasherize(payload.token)}-value`
const type = parseValueTypeDefinition(payload)
return {
type,
key,
name: camelize(key),
get defaultValue() { return defaultValueForDefinition(typeDefinition) },
get hasCustomDefaultValue() { return parseValueTypeDefault(typeDefinition) !== undefined },
get defaultValue() { return defaultValueForDefinition(payload.typeDefinition) },
get hasCustomDefaultValue() { return parseValueTypeDefault(payload.typeDefinition) !== undefined },
reader: readers[type],
writer: writers[type] || writers.default
}
Expand All @@ -169,7 +182,7 @@ const readers: { [type: string]: Reader } = {
array(value: string): any[] {
const array = JSON.parse(value)
if (!Array.isArray(array)) {
throw new TypeError("Expected array")
throw new TypeError(`expected value of type "array" but instead got value "${value}" of type "${parseValueTypeDefault(array)}"`)
}
return array
},
Expand All @@ -185,7 +198,7 @@ const readers: { [type: string]: Reader } = {
object(value: string): object {
const object = JSON.parse(value)
if (object === null || typeof object != "object" || Array.isArray(object)) {
throw new TypeError("Expected object")
throw new TypeError(`expected value of type "object" but instead got value "${value}" of type "${parseValueTypeDefault(object)}"`)
}
return object
},
Expand Down

0 comments on commit 5a60cde

Please sign in to comment.