Skip to content

Commit

Permalink
Allow ValueTypeObject to be provided as a Partial
Browse files Browse the repository at this point in the history
  • Loading branch information
marcoroth committed Feb 2, 2023
1 parent ac19c62 commit d4c9e7b
Show file tree
Hide file tree
Showing 4 changed files with 245 additions and 35 deletions.
4 changes: 3 additions & 1 deletion src/core/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { ActionDescriptor, parseActionDescriptorString, stringifyEventTarget } f
import { Token } from "../mutation-observers"
import { Schema } from "./schema"
import { camelize } from "./string_helpers"
import { hasProperty } from "./utils"

export class Action {
readonly element: Element
readonly index: number
Expand Down Expand Up @@ -54,7 +56,7 @@ export class Action {
return false
}

if (!Object.prototype.hasOwnProperty.call(this.keyMappings, standardFilter)) {
if (!hasProperty(this.keyMappings, standardFilter)) {
error(`contains unknown key filter: ${this.keyFilter}`)
}

Expand Down
7 changes: 7 additions & 0 deletions src/core/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function isSomething(object: any): boolean {
return object !== null && object !== undefined
}

export function hasProperty(object: any, property: string): boolean {
return Object.prototype.hasOwnProperty.call(object, property)
}
93 changes: 59 additions & 34 deletions src/core/value_properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Constructor } from "./constructor"
import { Controller } from "./controller"
import { readInheritableStaticObjectPairs } from "./inheritable_statics"
import { camelize, capitalize, dasherize } from "./string_helpers"
import { isSomething, hasProperty } from "./utils"

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

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

export type ValueTypeObject = { type: ValueTypeConstant; default: ValueTypeDefault }
export type ValueTypeObject = Partial<{ type: ValueTypeConstant; default: ValueTypeDefault }>

export type ValueTypeDefinition = ValueTypeConstant | ValueTypeDefault | ValueTypeObject

Expand All @@ -91,7 +92,7 @@ function parseValueDefinitionPair([token, typeDefinition]: ValueDefinitionPair,
})
}

function parseValueTypeConstant(constant: ValueTypeConstant) {
export function parseValueTypeConstant(constant?: ValueTypeConstant) {
switch (constant) {
case Array:
return "array"
Expand All @@ -106,7 +107,7 @@ function parseValueTypeConstant(constant: ValueTypeConstant) {
}
}

function parseValueTypeDefault(defaultValue: ValueTypeDefault) {
export function parseValueTypeDefault(defaultValue?: ValueTypeDefault) {
switch (typeof defaultValue) {
case "boolean":
return "boolean"
Expand All @@ -120,73 +121,97 @@ function parseValueTypeDefault(defaultValue: ValueTypeDefault) {
if (Object.prototype.toString.call(defaultValue) === "[object Object]") return "object"
}

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

export function parseValueTypeObject(payload: ValueTypeObjectPayload) {
const { controller, token, typeObject } = payload

const hasType = isSomething(typeObject.type)
const hasDefault = isSomething(typeObject.default)

if (!typeFromObject) return
const fullObject = hasType && hasDefault
const onlyType = hasType && !hasDefault
const onlyDefault = !hasType && hasDefault

const defaultValueType = parseValueTypeDefault(payload.typeObject.default)
const typeFromObject = parseValueTypeConstant(typeObject.type)
const typeFromDefaultValue = parseValueTypeDefault(payload.typeObject.default)

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

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

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}".`
`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}".`
)
}

return typeFromObject
if (fullObject) return typeFromObject
}

function parseValueTypeDefinition(payload: {
type ValueTypeDefinitionPayload = {
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)
}

export function parseValueTypeDefinition(payload: ValueTypeDefinitionPayload): ValueType {
const { controller, token, typeDefinition } = payload

const typeObject = { controller, token, typeObject: typeDefinition as ValueTypeObject }

const typeFromObject = parseValueTypeObject(typeObject as ValueTypeObjectPayload)
const typeFromDefaultValue = parseValueTypeDefault(typeDefinition as ValueTypeDefault)
const typeFromConstant = parseValueTypeConstant(typeDefinition as ValueTypeConstant)

const type = typeFromObject || typeFromDefaultValue || typeFromConstant

if (type) return type

const propertyPath = payload.controller ? `${payload.controller}.${payload.typeDefinition}` : payload.token
const propertyPath = controller ? `${controller}.${typeDefinition}` : token

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

function defaultValueForDefinition(typeDefinition: ValueTypeDefinition): ValueTypeDefault {
export function defaultValueForDefinition(typeDefinition: ValueTypeDefinition): ValueTypeDefault {
const constant = parseValueTypeConstant(typeDefinition as ValueTypeConstant)

if (constant) return defaultValuesByType[constant]

const defaultValue = (typeDefinition as ValueTypeObject).default
if (defaultValue !== undefined) return defaultValue
const hasDefault = hasProperty(typeDefinition, "default")
const hasType = hasProperty(typeDefinition, "type")
const typeObject = typeDefinition as ValueTypeObject

if (hasDefault) return typeObject.default!

if (hasType) {
const { type } = typeObject
const constantFromType = parseValueTypeConstant(type)

if (constantFromType) return defaultValuesByType[constantFromType]
}

return typeDefinition
}

function valueDescriptorForTokenAndTypeDefinition(payload: {
token: string
typeDefinition: ValueTypeDefinition
controller?: string
}) {
const key = `${dasherize(payload.token)}-value`
function valueDescriptorForTokenAndTypeDefinition(payload: ValueTypeDefinitionPayload) {
const { token, typeDefinition } = payload

const key = `${dasherize(token)}-value`
const type = parseValueTypeDefinition(payload)
return {
type,
key,
name: camelize(key),
get defaultValue() {
return defaultValueForDefinition(payload.typeDefinition)
return defaultValueForDefinition(typeDefinition)
},
get hasCustomDefaultValue() {
return parseValueTypeDefault(payload.typeDefinition) !== undefined
return parseValueTypeDefault(typeDefinition) !== undefined
},
reader: readers[type],
writer: writers[type] || writers.default,
Expand Down
176 changes: 176 additions & 0 deletions src/tests/modules/core/value_properties_tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { ValueController } from "../../controllers/value_controller"
import { ControllerTestCase } from "../../cases/controller_test_case"

import {
parseValueTypeDefault,
parseValueTypeConstant,
parseValueTypeObject,
parseValueTypeDefinition,
defaultValueForDefinition,
} from "../../../core/value_properties"

export default class ValuePropertiesTests extends ControllerTestCase(ValueController) {
"test parseValueTypeConstant"() {
this.assert.equal(parseValueTypeConstant(String), "string")
this.assert.equal(parseValueTypeConstant(Boolean), "boolean")
this.assert.equal(parseValueTypeConstant(Array), "array")
this.assert.equal(parseValueTypeConstant(Object), "object")
this.assert.equal(parseValueTypeConstant(Number), "number")

this.assert.equal(parseValueTypeConstant("" as any), undefined)
this.assert.equal(parseValueTypeConstant({} as any), undefined)
this.assert.equal(parseValueTypeConstant([] as any), undefined)
this.assert.equal(parseValueTypeConstant(true as any), undefined)
this.assert.equal(parseValueTypeConstant(false as any), undefined)
this.assert.equal(parseValueTypeConstant(0 as any), undefined)
this.assert.equal(parseValueTypeConstant(1 as any), undefined)
this.assert.equal(parseValueTypeConstant(null!), undefined)
this.assert.equal(parseValueTypeConstant(undefined), undefined)
}

"test parseValueTypeDefault"() {
this.assert.equal(parseValueTypeDefault(""), "string")
this.assert.equal(parseValueTypeDefault("Some string"), "string")

this.assert.equal(parseValueTypeDefault(true), "boolean")
this.assert.equal(parseValueTypeDefault(false), "boolean")

this.assert.equal(parseValueTypeDefault([]), "array")
this.assert.equal(parseValueTypeDefault([1, 2, 3]), "array")
this.assert.equal(parseValueTypeDefault([true, false, true]), "array")
this.assert.equal(parseValueTypeDefault([{}, {}, {}]), "array")

this.assert.equal(parseValueTypeDefault({}), "object")
this.assert.equal(parseValueTypeDefault({ one: "key" }), "object")

this.assert.equal(parseValueTypeDefault(-1), "number")
this.assert.equal(parseValueTypeDefault(0), "number")
this.assert.equal(parseValueTypeDefault(1), "number")
this.assert.equal(parseValueTypeDefault(-0.1), "number")
this.assert.equal(parseValueTypeDefault(0.0), "number")
this.assert.equal(parseValueTypeDefault(0.1), "number")

this.assert.equal(parseValueTypeDefault(null!), undefined)
this.assert.equal(parseValueTypeDefault(undefined!), undefined)
}

"test parseValueTypeObject"() {
const typeObject = (object: any) => {
return parseValueTypeObject({
controller: this.controller.identifier,
token: "url",
typeObject: object,
})
}

this.assert.equal(typeObject({ type: String, default: "" }), "string")
this.assert.equal(typeObject({ type: String, default: "123" }), "string")
this.assert.equal(typeObject({ type: String }), "string")
this.assert.equal(typeObject({ default: "" }), "string")
this.assert.equal(typeObject({ default: "123" }), "string")

this.assert.equal(typeObject({ type: Number, default: 0 }), "number")
this.assert.equal(typeObject({ type: Number, default: 1 }), "number")
this.assert.equal(typeObject({ type: Number, default: -1 }), "number")
this.assert.equal(typeObject({ type: Number }), "number")
this.assert.equal(typeObject({ default: 0 }), "number")
this.assert.equal(typeObject({ default: 1 }), "number")
this.assert.equal(typeObject({ default: -1 }), "number")

this.assert.equal(typeObject({ type: Array, default: [] }), "array")
this.assert.equal(typeObject({ type: Array, default: [1] }), "array")
this.assert.equal(typeObject({ type: Array }), "array")
this.assert.equal(typeObject({ default: [] }), "array")
this.assert.equal(typeObject({ default: [1] }), "array")

this.assert.equal(typeObject({ type: Object, default: {} }), "object")
this.assert.equal(typeObject({ type: Object, default: { some: "key" } }), "object")
this.assert.equal(typeObject({ type: Object }), "object")
this.assert.equal(typeObject({ default: {} }), "object")
this.assert.equal(typeObject({ default: { some: "key" } }), "object")

this.assert.equal(typeObject({ type: Boolean, default: true }), "boolean")
this.assert.equal(typeObject({ type: Boolean, default: false }), "boolean")
this.assert.equal(typeObject({ type: Boolean }), "boolean")
this.assert.equal(typeObject({ default: false }), "boolean")

this.assert.throws(() => typeObject({ type: Boolean, default: "something else" }), {
name: "Error",
message: `The specified default value for the Stimulus Value "test.url" must match the defined type "boolean". The provided default value of "something else" is of type "string".`,
})

this.assert.throws(() => typeObject({ type: Boolean, default: "true" }), {
name: "Error",
message: `The specified default value for the Stimulus Value "test.url" must match the defined type "boolean". The provided default value of "true" is of type "string".`,
})
}

"test parseValueTypeDefinition booleans"() {
const typeDefinition = (definition: any) => {
return parseValueTypeDefinition({
controller: this.controller.identifier,
token: "url",
typeDefinition: definition,
})
}

this.assert.equal(typeDefinition(Boolean), "boolean")
this.assert.equal(typeDefinition(true), "boolean")
this.assert.equal(typeDefinition(false), "boolean")
this.assert.equal(typeDefinition({ type: Boolean, default: false }), "boolean")
this.assert.equal(typeDefinition({ type: Boolean }), "boolean")
this.assert.equal(typeDefinition({ default: true }), "boolean")

// since the provided value is actually an object, it's going to be of type "object"
this.assert.equal(typeDefinition({ default: null }), "object")
this.assert.equal(typeDefinition({ default: undefined }), "object")

this.assert.equal(typeDefinition({}), "object")
this.assert.equal(typeDefinition(""), "string")
this.assert.equal(typeDefinition([]), "array")

this.assert.throws(() => typeDefinition(null))
this.assert.throws(() => typeDefinition(undefined))
}

"test defaultValueForDefinition"() {
this.assert.deepEqual(defaultValueForDefinition(String), "")
this.assert.deepEqual(defaultValueForDefinition(Boolean), false)
this.assert.deepEqual(defaultValueForDefinition(Object), {})
this.assert.deepEqual(defaultValueForDefinition(Array), [])
this.assert.deepEqual(defaultValueForDefinition(Number), 0)

this.assert.deepEqual(defaultValueForDefinition({ type: String }), "")
this.assert.deepEqual(defaultValueForDefinition({ type: Boolean }), false)
this.assert.deepEqual(defaultValueForDefinition({ type: Object }), {})
this.assert.deepEqual(defaultValueForDefinition({ type: Array }), [])
this.assert.deepEqual(defaultValueForDefinition({ type: Number }), 0)

this.assert.deepEqual(defaultValueForDefinition({ type: String, default: null }), null)
this.assert.deepEqual(defaultValueForDefinition({ type: Boolean, default: null }), null)
this.assert.deepEqual(defaultValueForDefinition({ type: Object, default: null }), null)
this.assert.deepEqual(defaultValueForDefinition({ type: Array, default: null }), null)
this.assert.deepEqual(defaultValueForDefinition({ type: Number, default: null }), null)

this.assert.deepEqual(defaultValueForDefinition({ type: String, default: "some string" }), "some string")
this.assert.deepEqual(defaultValueForDefinition({ type: Boolean, default: true }), true)
this.assert.deepEqual(defaultValueForDefinition({ type: Object, default: { some: "key" } }), { some: "key" })
this.assert.deepEqual(defaultValueForDefinition({ type: Array, default: [1, 2, 3] }), [1, 2, 3])
this.assert.deepEqual(defaultValueForDefinition({ type: Number, default: 99 }), 99)

this.assert.deepEqual(defaultValueForDefinition("some string"), "some string")
this.assert.deepEqual(defaultValueForDefinition(true), true)
this.assert.deepEqual(defaultValueForDefinition({ some: "key" }), { some: "key" })
this.assert.deepEqual(defaultValueForDefinition([1, 2, 3]), [1, 2, 3])
this.assert.deepEqual(defaultValueForDefinition(99), 99)

this.assert.deepEqual(defaultValueForDefinition({ default: "some string" }), "some string")
this.assert.deepEqual(defaultValueForDefinition({ default: true }), true)
this.assert.deepEqual(defaultValueForDefinition({ default: { some: "key" } }), { some: "key" })
this.assert.deepEqual(defaultValueForDefinition({ default: [1, 2, 3] }), [1, 2, 3])
this.assert.deepEqual(defaultValueForDefinition({ default: 99 }), 99)

this.assert.deepEqual(defaultValueForDefinition({ default: null }), null)
this.assert.deepEqual(defaultValueForDefinition({ default: undefined }), undefined)
}
}

0 comments on commit d4c9e7b

Please sign in to comment.