zcb 0.0.7 → 0.0.9

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.
@@ -2,6 +2,7 @@ import { type JSONSchema7 } from 'json-schema';
2
2
  import { type ZodError, type z } from 'zod';
3
3
  import { zodToJsonSchema } from 'zod-to-json-schema';
4
4
  import { cloneNonEnumerableValues } from './transformers/cloneNonEnumerableValues.ts';
5
+ import { NonEmumeralProperties } from './types.ts';
5
6
  import { arrayHasInvalidDefaults } from './utils/arrayHasInvalidDefaults.ts';
6
7
  import { isDerivedValueCallback } from './utils/isDerivedValueCallback.ts';
7
8
  import { isInvalidPropertyOverride } from './utils/isInvalidPropertyOverride.ts';
@@ -53,7 +54,7 @@ export const createConfigBuilder = <ZodTypes>(
53
54
 
54
55
  let config = initialValues as Config;
55
56
 
56
- Object.defineProperty(config, '__zcb', {
57
+ Object.defineProperty(config, NonEmumeralProperties.ZCB, {
57
58
  configurable: false,
58
59
  enumerable: false,
59
60
  value: true,
@@ -63,7 +64,7 @@ export const createConfigBuilder = <ZodTypes>(
63
64
 
64
65
  const configBuilder = {
65
66
  disable: () => {
66
- Object.defineProperty(config, '__disabled', {
67
+ Object.defineProperty(config, NonEmumeralProperties.DISABLED, {
67
68
  configurable: false,
68
69
  enumerable: false,
69
70
  value: true,
@@ -80,7 +81,7 @@ export const createConfigBuilder = <ZodTypes>(
80
81
  }
81
82
  },
82
83
  experiment: (key: string) => {
83
- Object.defineProperty(config, '__experiment', {
84
+ Object.defineProperty(config, NonEmumeralProperties.EXPERIMENT, {
84
85
  configurable: false,
85
86
  enumerable: false,
86
87
  value: key,
@@ -97,7 +98,7 @@ export const createConfigBuilder = <ZodTypes>(
97
98
  const values = configBuilder.values();
98
99
  config = {} as Config;
99
100
 
100
- Object.defineProperty(config, '__zcb', {
101
+ Object.defineProperty(config, NonEmumeralProperties.ZCB, {
101
102
  configurable: false,
102
103
  enumerable: false,
103
104
  value: true,
@@ -129,7 +130,7 @@ export const createConfigBuilder = <ZodTypes>(
129
130
  },
130
131
  } as unknown as ConfigBuilder;
131
132
 
132
- Object.defineProperty(configBuilder, '__callbacks', {
133
+ Object.defineProperty(configBuilder, NonEmumeralProperties.CALLBACKS, {
133
134
  configurable: false,
134
135
  enumerable: false,
135
136
  value: callbacks,
@@ -0,0 +1,16 @@
1
+ import { runExperiments } from './transformers/runExperiments.ts';
2
+ import type { ConfigParserOptions, TransformConfigHandler } from './types.ts';
3
+ import { transformConfig } from './utils/transformConfig.ts';
4
+
5
+ export const createConfigParser = async <Config extends object>(
6
+ config: Config,
7
+ { experimentsCallback }: ConfigParserOptions = {}
8
+ ) => {
9
+ const handlers: TransformConfigHandler[] = [];
10
+
11
+ if (experimentsCallback) {
12
+ handlers.push(runExperiments(experimentsCallback));
13
+ }
14
+
15
+ return handlers.length > 0 ? await transformConfig(config, handlers) : config;
16
+ };
@@ -1,19 +1,17 @@
1
- import { config } from './__testUtils__/config.ts';
1
+ import { createReader } from './__testUtils__/configReader.ts';
2
2
 
3
3
  describe('createConfigReader', () => {
4
4
  describe('when a user accesses a known property', () => {
5
- it('should return the correct value', async () => {
6
- const { createConfigReader } = await import('./createConfigReader.ts');
7
- const reader = createConfigReader(config);
5
+ it('should return the correct value', () => {
6
+ const reader = createReader();
8
7
  const value = reader('countryCode');
9
8
  expect(value).toBe('GB');
10
9
  });
11
10
  });
12
11
 
13
12
  describe('when a user accesses a known nested property', () => {
14
- it('should return the correct value', async () => {
15
- const { createConfigReader } = await import('./createConfigReader.ts');
16
- const reader = createConfigReader(config);
13
+ it('should return the correct value', () => {
14
+ const reader = createReader();
17
15
  const value = reader('pages.contactDetails.name');
18
16
  expect(value).toBe('contactDetails');
19
17
  });
@@ -21,9 +19,8 @@ describe('createConfigReader', () => {
21
19
 
22
20
  describe('when the reader is scoped', () => {
23
21
  describe('when a user accesses a known property', () => {
24
- it('should return the correct value', async () => {
25
- const { createConfigReader } = await import('./createConfigReader.ts');
26
- const reader = createConfigReader(config);
22
+ it('should return the correct value', () => {
23
+ const reader = createReader();
27
24
  const scopedReader = reader.scope('pages.contactDetails');
28
25
  const value = scopedReader('name');
29
26
  expect(value).toBe('contactDetails');
@@ -33,9 +30,8 @@ describe('createConfigReader', () => {
33
30
 
34
31
  describe('when the reader is scoped multiple times', () => {
35
32
  describe('when a user accesses a known property', () => {
36
- it('should return the correct value', async () => {
37
- const { createConfigReader } = await import('./createConfigReader.ts');
38
- const reader = createConfigReader(config);
33
+ it('should return the correct value', () => {
34
+ const reader = createReader();
39
35
  const scopedReader = reader.scope('pages.contactDetails').scope('sections.1.sections').scope('0');
40
36
  const value = scopedReader('name');
41
37
  expect(value).toBe('main');
@@ -1,12 +1,9 @@
1
1
  import get from 'lodash/get.js';
2
- import type { Get, Join } from 'type-fest';
3
- import type { Leaves, Paths } from './types.ts';
2
+ import type { Get } from 'type-fest';
3
+ import type { Path, Scope } from './types.ts';
4
4
 
5
5
  export const createConfigReader = <Config extends object>(config: Config) => {
6
- const configReader = <Path extends Join<Leaves<Config>, '.'>>(path: Path) => get(config, path);
7
-
8
- configReader.scope = <Scope extends Join<Paths<Config>, '.'>>(scope: Scope) =>
9
- createConfigReader(get(config, scope) as Get<Config, Scope>);
10
-
6
+ const configReader = <P extends Path<Config>>(path: P) => get(config, path);
7
+ configReader.scope = <S extends Scope<Config>>(scope: S) => createConfigReader(get(config, scope) as Get<Config, S>);
11
8
  return configReader;
12
9
  };
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './createConfigBuilder.ts';
2
+ export * from './createConfigParser.ts';
2
3
  export * from './createConfigReader.ts';
3
4
  export * from './data/countryCodes.ts';
4
5
  export * from './data/countryNames.ts';
@@ -1,6 +1,6 @@
1
- import type { AnyRecord, TransformConfigHandlerSync } from '../types.ts';
1
+ import { type AnyRecord, NonEmumeralProperties, type TransformConfigHandlerSync } from '../types.ts';
2
2
 
3
- const NON_ENUMERABLE_KEYS = new Set(['__disabled', '__experiment', '__zcb']);
3
+ const NON_ENUMERABLE_KEYS = new Set(Object.values(NonEmumeralProperties));
4
4
 
5
5
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
6
  export const cloneNonEnumerableValues: TransformConfigHandlerSync = <Config extends AnyRecord>(
@@ -1,11 +1,16 @@
1
- import { type AnyRecord, TransformConfigHandlerAction, type TransformConfigHandlerSync } from '../types.ts';
1
+ import {
2
+ type AnyRecord,
3
+ NonEmumeralProperties,
4
+ TransformConfigHandlerAction,
5
+ type TransformConfigHandlerSync,
6
+ } from '../types.ts';
2
7
 
3
8
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
9
  export const removeDisabledSlices: TransformConfigHandlerSync = <Config extends AnyRecord>(
5
10
  clone: Config,
6
11
  config: Config
7
12
  ) => {
8
- if ('__disabled' in config) {
13
+ if (NonEmumeralProperties.DISABLED in config) {
9
14
  return {
10
15
  action: TransformConfigHandlerAction.DELETE_NODE,
11
16
  };
@@ -0,0 +1,24 @@
1
+ import {
2
+ type AnyRecord,
3
+ type Buckets,
4
+ NonEmumeralProperties,
5
+ type RunExperimentsCallback,
6
+ type TransformConfigHandler,
7
+ } from '../types.ts';
8
+
9
+ export const runExperiments =
10
+ (callback: RunExperimentsCallback): TransformConfigHandler =>
11
+ async <Config extends AnyRecord>(clone: Config, config: Config) => {
12
+ if (NonEmumeralProperties.EXPERIMENT in config && typeof config.__experiment === 'object') {
13
+ const { buckets, id } = config.__experiment as { buckets: Buckets<Config>; id: string };
14
+ const bucket = await callback(id);
15
+
16
+ if (bucket && bucket in buckets && buckets[bucket]) {
17
+ return buckets[bucket]!;
18
+ }
19
+ }
20
+
21
+ return {
22
+ value: clone,
23
+ };
24
+ };
@@ -1,16 +1,20 @@
1
- import type { AnyRecord, ExperimentsCallback, TransformConfigHandler } from '../types.ts';
1
+ import {
2
+ type AnyRecord,
3
+ NonEmumeralProperties,
4
+ type SetupExperimentsCallback,
5
+ type TransformConfigHandler,
6
+ } from '../types.ts';
2
7
 
3
8
  export const setupExperiments =
4
- (callback: ExperimentsCallback): TransformConfigHandler =>
9
+ (callback: SetupExperimentsCallback): TransformConfigHandler =>
5
10
  async <Config extends AnyRecord>(clone: Config, config: Config) => {
6
- if ('__experiment' in config && typeof config.__experiment === 'string') {
7
- const { action, value = {} } = await callback(config.__experiment, clone, config);
11
+ if (NonEmumeralProperties.EXPERIMENT in config && typeof config.__experiment === 'string') {
12
+ const buckets = await callback(config.__experiment, clone, config);
8
13
 
9
14
  // @ts-expect-error private property
10
15
  clone.__experiment = {
11
- action,
16
+ buckets,
12
17
  id: config.__experiment,
13
- value,
14
18
  };
15
19
 
16
20
  return {
package/src/types.ts CHANGED
@@ -1,19 +1,38 @@
1
1
  import type { List } from 'ts-toolbelt';
2
- import type { Includes } from 'type-fest';
2
+ import type { Includes, Join } from 'type-fest';
3
3
 
4
4
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
5
5
  export type AnyRecord = Record<string, any>;
6
6
 
7
- export interface WriteConfigOptions {
8
- experimentsCallback?: ExperimentsCallback;
9
- outputFile: string;
7
+ export type Buckets<Config extends AnyRecord> = Record<string, Experiment<Config>>;
8
+
9
+ export interface ConfigParserOptions {
10
+ experimentsCallback?: RunExperimentsCallback;
11
+ }
12
+
13
+ export interface Experiment<Config extends AnyRecord> {
14
+ action?: TransformConfigHandlerAction;
15
+ value?: Config;
16
+ }
17
+
18
+ export enum NonEmumeralProperties {
19
+ CALLBACKS = '__callbacks',
20
+ DISABLED = '__disabled',
21
+ EXPERIMENT = '__experiment',
22
+ ZCB = '__zcb',
10
23
  }
11
24
 
12
- export type ExperimentsCallback = <Config extends AnyRecord>(
25
+ export type Path<Config extends object> = Join<Leaves<Config>, '.'>;
26
+
27
+ export type RunExperimentsCallback = (id: string) => Promise<string>;
28
+
29
+ export type Scope<Config extends object> = Join<Paths<Config>, '.'>;
30
+
31
+ export type SetupExperimentsCallback = <Config extends AnyRecord>(
13
32
  id: string,
14
33
  clone: Config,
15
34
  config: Config
16
- ) => Promise<TransformConfigHandlerReturnType<Config>>;
35
+ ) => Promise<Buckets<Config>>;
17
36
 
18
37
  export type TransformConfigHandler = <Config extends AnyRecord>(
19
38
  clone: Config,
@@ -35,6 +54,11 @@ export interface TransformConfigHandlerReturnType<Config extends AnyRecord> {
35
54
  value?: Config;
36
55
  }
37
56
 
57
+ export interface WriteConfigOptions {
58
+ experimentsCallback?: SetupExperimentsCallback;
59
+ outputFile: string;
60
+ }
61
+
38
62
  export type Leaves<T, Path extends string[] = []> = T extends string
39
63
  ? Path
40
64
  : {
@@ -1,10 +1,10 @@
1
1
  import { resolve } from 'node:path';
2
2
  import shelljs from 'shelljs';
3
3
  import type { Commands } from '../cli.ts';
4
- import type { ExperimentsCallback } from '../types.ts';
5
- import { writeConfig } from './writeConfig.ts';
4
+ import type { SetupExperimentsCallback } from '../types.ts';
5
+ import { transformWriteConfig } from './transformWriteConfig.ts';
6
6
 
7
- export const importValidateWriteConfig = (
7
+ export const importValidateTransformWriteConfig = (
8
8
  inputFile: string,
9
9
  outputFile: string,
10
10
  command: Commands,
@@ -19,6 +19,7 @@ export const importValidateWriteConfig = (
19
19
  }) => {
20
20
  if (!configBuilder.validate()) {
21
21
  shelljs.echo(`zcd ${command} => invalid config`);
22
+ shelljs.echo(`zcd ${command} => config values:\n${configBuilder.toJson()}\n`);
22
23
  shelljs.echo(`zcd ${command} => errors:\n${JSON.stringify(configBuilder.errors(), undefined, 2)}\n`);
23
24
  shelljs.exit(1);
24
25
  }
@@ -28,8 +29,8 @@ export const importValidateWriteConfig = (
28
29
 
29
30
  if (experimentCallbackFile) {
30
31
  import(resolve(process.cwd(), experimentCallbackFile))
31
- .then(({ default: experimentsCallback }: { default: ExperimentsCallback }) => {
32
- void writeConfig(configBuilder.values(), { experimentsCallback, outputFile });
32
+ .then(({ default: experimentsCallback }: { default: SetupExperimentsCallback }) => {
33
+ void transformWriteConfig(configBuilder.values(), { experimentsCallback, outputFile });
33
34
  })
34
35
  .catch((error: unknown) => {
35
36
  if (error instanceof Error) {
@@ -44,7 +45,7 @@ export const importValidateWriteConfig = (
44
45
  return;
45
46
  }
46
47
 
47
- void writeConfig(configBuilder.values(), { outputFile });
48
+ void transformWriteConfig(configBuilder.values(), { outputFile });
48
49
  }
49
50
  )
50
51
  .catch((error: unknown) => {
@@ -9,7 +9,7 @@ import { setupExperiments } from '../transformers/setupExperiments.ts';
9
9
  import type { TransformConfigHandler, WriteConfigOptions } from '../types.ts';
10
10
  import { transformConfig } from './transformConfig.ts';
11
11
 
12
- export const writeConfig = async <Config extends object>(
12
+ export const transformWriteConfig = async <Config extends object>(
13
13
  config: Config,
14
14
  { experimentsCallback, outputFile }: WriteConfigOptions
15
15
  ) => {