From 5264bd412e6f4526aa8634f87037ba393a7dfc2a Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Sun, 2 Feb 2025 15:43:17 -0500 Subject: [PATCH] non-hkt computed properties --- src/client/client.ts | 10 +++++ src/client/properties/configuration.ts | 6 +-- src/client/properties/extensions.test.ts | 23 +++++----- src/client/properties/properties.test.ts | 43 +++++++++++++++--- src/client/properties/properties.ts | 55 ++++++++++++++++++++---- src/types/context.ts | 4 +- 6 files changed, 110 insertions(+), 31 deletions(-) diff --git a/src/client/client.ts b/src/client/client.ts index 8efed1d9..570b1a4c 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -218,6 +218,16 @@ export const createWithContext = <$Context extends Context>( Object.assign(client, context.properties.static) + context.properties.computed.forEach(propertiesComputer => { + Object.assign( + client, + propertiesComputer({ + context, + client, + }), + ) + }) + // todo: access computed properties from context context.extensions.forEach(_ => { const configurationIndex = context.configuration as ConfigurationIndex diff --git a/src/client/properties/configuration.ts b/src/client/properties/configuration.ts index 1c6e8e5e..b3d02098 100644 --- a/src/client/properties/configuration.ts +++ b/src/client/properties/configuration.ts @@ -69,15 +69,15 @@ export interface ContextFragmentConfiguration { readonly configuration: { readonly output: { readonly configurator: Configurators.Output.OutputConfigurator - readonly current: Configurators.Output.OutputConfigurator['default'] + readonly current: Configurators.Output.OutputConfigurator['normalizedIncremental'] } readonly check: { readonly configurator: Configurators.Check.CheckConfigurator - readonly current: Configurators.Check.CheckConfigurator['default'] + readonly current: Configurators.Check.CheckConfigurator['normalizedIncremental'] } readonly schema: { readonly configurator: Configurators.Schema.SchemaConfigurator - readonly current: Configurators.Schema.SchemaConfigurator['default'] + readonly current: Configurators.Schema.SchemaConfigurator['normalizedIncremental'] } } } diff --git a/src/client/properties/extensions.test.ts b/src/client/properties/extensions.test.ts index 43a885fd..f6565a94 100644 --- a/src/client/properties/extensions.test.ts +++ b/src/client/properties/extensions.test.ts @@ -35,6 +35,18 @@ describe(`transport`, () => { }) }) +describe(`properties`, () => { + const properties1 = { foo: `bar` } + + test(`can be added (static)`, ({ g0 }) => { + const BExtension = Extension(`BExtension`).properties(properties1).return() + const g1a = g0.use(BExtension()) + const g1b = g0.properties(properties1) + expect(g1a._.properties).toEqual(g1b._.properties) + expectTypeOf(g1a._.properties).toEqualTypeOf(g1b._.properties) + }) +}) + describe(`request interceptor`, () => { test(`can be added`, ({ g0 }) => { const i1 = createInterceptor(async ({ pack }) => { @@ -50,17 +62,6 @@ describe(`request interceptor`, () => { expectTypeOf(g1a._.requestPipelineInterceptors).toEqualTypeOf(g1b._.requestPipelineInterceptors) }) }) -describe(`properties`, () => { - const properties1 = { foo: `bar` } - - test(`can be added (statically given)`, ({ g0 }) => { - const BExtension = Extension(`BExtension`).properties(properties1).return() - const g1a = g0.use(BExtension()) - const g1b = g0.properties(properties1) - expect(g1a._.properties).toEqual(g1b._.properties) - expectTypeOf(g1a._.properties).toEqualTypeOf(g1b._.properties) - }) -}) // test('using an extension without type hooks leaves them empty', () => { // const Ex = Extension('test').done() diff --git a/src/client/properties/properties.test.ts b/src/client/properties/properties.test.ts index 1b62c0ec..1ec16ede 100644 --- a/src/client/properties/properties.test.ts +++ b/src/client/properties/properties.test.ts @@ -1,8 +1,10 @@ -import { expect, expectTypeOf } from 'vitest' +import { describe, expect, expectTypeOf } from 'vitest' import { test } from '../../../tests/_/helpers.js' import { Context } from '../../types/context.js' +import { createPropertiesComputer } from './properties.js' -const properties1 = { foo: `bar` } +const propertiesStatic1 = { foo: `bar` } +// const propertiesComputer1 = createPropertiesComputer((parameters) => ({ parameters })) test(`initial context is empty`, ({ g0 }) => { expect(g0._.properties).toEqual(Context.States.empty.properties) @@ -10,11 +12,38 @@ test(`initial context is empty`, ({ g0 }) => { }) test(`can add static properties`, ({ g0 }) => { - const g1 = g0.properties(properties1) + const g1 = g0.properties(propertiesStatic1) // Context extended - expect(g1._.properties.static).toEqual(properties1) - expectTypeOf(g1._.properties.static).toMatchTypeOf() + expect(g1._.properties.static).toEqual(propertiesStatic1) + expectTypeOf(g1._.properties.static).toMatchTypeOf() // Client extended - expect(g1).toMatchObject(properties1) - expectTypeOf(g1.foo).toMatchTypeOf() + expect(g1).toMatchObject(propertiesStatic1) + expectTypeOf(g1.foo).toMatchTypeOf() +}) + +describe(`computed properties`, () => { + test(`can be added`, ({ g0 }) => { + const computer = createPropertiesComputer()((parameters) => ({ parameters })) + const g1 = g0.properties(computer) + // Context extended + expect(g1._.properties.static).toEqual({}) + expect(g1._.properties.computed).toEqual([computer]) + expectTypeOf(g1._.properties.static).toMatchTypeOf<{ parameters: { context: typeof g0._; client: typeof g0 } }>() + expectTypeOf(g1._.properties.computed).toEqualTypeOf() + // Client extended + expect(g1.parameters.client).toBe(g1) + expect(g1.parameters.context).toEqual(g1._) + }) + test(`Are computed every time the client is copied`, ({ g0 }) => { + const computer = createPropertiesComputer()((parameters) => ({ + preflightCheckNow: parameters.context.configuration.check.current.preflight, + })) + const g1 = g0.properties(computer) + const g2 = g1.with({ check: { preflight: false } }) + const g3 = g2.with({ check: { preflight: true } }) + expect(g1.preflightCheckNow).toEqual(true) + expect(g2.preflightCheckNow).toEqual(false) + expect(g3.preflightCheckNow).toEqual(true) + }) + // todo: test computed properties that use HKT to also compute at the type level. }) diff --git a/src/client/properties/properties.ts b/src/client/properties/properties.ts index f234d861..03c76297 100644 --- a/src/client/properties/properties.ts +++ b/src/client/properties/properties.ts @@ -14,25 +14,51 @@ import type { Client } from '../client.js' // Method // ------------------------------------------------------------ +// todo have the client type be passed through too? Using `this` from parent? export interface AddPropertiesMethod<$Context extends Context> { - <$PropertiesStatic extends PropertiesStatic>(propertiesStatic: $PropertiesStatic): Client< + <$Properties extends Properties>(properties: $Properties | PropertiesComputer<$Context, $Properties>): Client< { - [_ in keyof $Context]: _ extends 'properties' ? ContextFragmentAddProperties<$Context, $PropertiesStatic> + [_ in keyof $Context]: _ extends 'properties' ? ContextFragmentAddProperties<$Context, $Properties> : $Context[_] }, {} > } +// ------------------------------------------------------------ +// Helpers +// ------------------------------------------------------------ + +export type PropertiesComputer<$Context extends Context, $Properties extends Properties> = ( + parameters: { + context: $Context + client: Client<$Context, {}> + }, +) => $Properties + +export const createPropertiesComputer = < + $Client extends { _: Context }, +>() => +< + $PropertiesComputer extends ( + parameters: { + context: $Client['_'] + client: $Client + }, + ) => Properties, +>( + propertiesComputer: $PropertiesComputer, +) => propertiesComputer + // ------------------------------------------------------------ // Context Fragment // ------------------------------------------------------------ -export type PropertiesStatic = object +export type Properties = object export interface ContextFragmentProperties { readonly properties: { - readonly static: PropertiesStatic + readonly static: Properties readonly computed: ReadonlyArray< <$Context extends Context>(parameters: Extension.ConstructorParameters<$Context>) => object > @@ -55,7 +81,7 @@ export const contextFragmentPropertiesEmpty: ContextFragmentPropertiesEmpty = Ob export type ContextFragmentAddProperties< $Context extends Context, - $PropertiesStatic extends PropertiesStatic, + $PropertiesStatic extends Properties, __NewStaticProperties = ObjectMergeShallow<$Context['properties']['static'], $PropertiesStatic>, __NewContextProperties = { static: __NewStaticProperties @@ -67,17 +93,30 @@ export type ContextFragmentAddProperties< // ContextReducer // ------------------------------------------------------------ +type MethodArguments = Properties | PropertiesComputer + export const contextFragmentPropertiesAdd = <$Context extends Context>( context: $Context, - propertiesStatic: PropertiesStatic, + propertiesInput: MethodArguments, ): null | ContextFragmentProperties => { - if (isObjectEmpty(propertiesStatic)) return null + if (typeof propertiesInput === `function`) { + const properties = { + ...context.properties, + computed: [ + ...context.properties.computed, + propertiesInput, + ], + } + return { properties } + } + + if (isObjectEmpty(propertiesInput)) return null const properties = { ...context.properties, static: Object.freeze({ ...context.properties.static, - ...propertiesStatic, + ...propertiesInput, }), } diff --git a/src/types/context.ts b/src/types/context.ts index d874e8fb..e4fe327f 100644 --- a/src/types/context.ts +++ b/src/types/context.ts @@ -30,12 +30,12 @@ import { type EmptyArray, type ObjectMergeShallow } from '../lib/prelude.js' export interface Context extends + ContextFragmentConfiguration, ContextFragmentTransports, ContextFragmentProperties, ContextFragmentRequestInterceptors, ContextFragmentExtensions, - ContextFragmentScalars, - ContextFragmentConfiguration + ContextFragmentScalars { // Type Level Properties /**