Skip to content

Commit

Permalink
feat(environment): add required option to query.get
Browse files Browse the repository at this point in the history
  • Loading branch information
RicardoJBarrios committed Apr 12, 2023
1 parent 201cebf commit 38e86f9
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ describe('EnvironmentQuery', () => {
const expected = m.cold('-a---b---a-|', { a: '0', b: undefined });
jest.spyOn(store, 'getAll$').mockReturnValue(source);

m.expect(query.get$('a.a', { targetType })).toBeObservable(expected);
m.expect(query.get$<any>('a.a', { targetType })).toBeObservable(expected);
})
);

Expand Down Expand Up @@ -318,6 +318,21 @@ describe('EnvironmentQuery', () => {
expect(query.get('a.a', { targetType })).toEqual('0');
});

it(`get(path,{required:true}) returns environment property at path if exists`, () => {
expect(query.get('a.a', { required: true })).toEqual(0);
});

it(`get(path,{required:true}) throws error if the path cannot be resolved`, () => {
expect(() => query.get('a.z', { required: true })).toThrowWithMessage(
ReferenceError,
'The environment property "a.z" is not defined'
);
});

it(`get(path,{required:false}) returns undefined if the path cannot be resolved`, () => {
expect(query.get('a.z', { required: false })).toBeUndefined();
});

it(`get(path,{transpile}) returns the transpiled environment property at path`, () => {
const transpile = { a: { a: 2 } };

Expand Down
54 changes: 31 additions & 23 deletions packages/environment/src/lib/query/environment-query.application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ import { EnvironmentState, EnvironmentStore, Property } from '../store';
import { EnvironmentQuery } from './environment-query.interface';
import { EnvironmentQueryConfig } from './environment-query-config.interface';
import { environmentQueryConfigFactory } from './environment-query-config-factory.function';
import { GetOptions } from './get-options.interface';
import { GetProperty } from './get-property.type';
import { GetOptions, GetOptionsAsync, GetOptionsObs } from './get-options.interface';

/**
* Gets the properties from the environment.
Expand Down Expand Up @@ -119,19 +118,22 @@ export class DefaultEnvironmentQuery implements EnvironmentQuery {
return containsList.some((contains: boolean) => contains);
}

get$<T = Property>(path: Path, options?: GetOptions<T>): Observable<T | undefined> {
get$<T extends Property, K = T>(path: Path, options?: GetOptionsObs<T, K>): Observable<T | K | undefined> {
return this.getAll$().pipe(
map((state: EnvironmentState) => this.getProperty(state, path)),
map((property?: Property) => this.getDefaultValue(property, options?.defaultValue)),
map((property?: Property) => this.getTargetType(property, options?.targetType)),
map((property?: Property | T) => this.getTranspile(property, options?.transpile, options?.config)),
map((property?: unknown) => this.getDefaultValue(property, options?.defaultValue)),
map((property?: unknown) => this.getTargetType(property, options?.targetType)),
map((property?: unknown) => this.getTranspile(property, options?.transpile, options?.config)),
distinctUntilChanged(isEqual)
);
}

getAsync<T = Property>(path: Path, options?: GetOptions<T>): Promise<T | undefined> {
const get$: Observable<T | undefined> = this.get$<T>(path, options).pipe(filterNil());
const getAsync: Promise<T | undefined> = firstValueFrom(get$);
getAsync<T extends Property, K = T>(
path: Path,
options?: GetOptionsAsync<T, K> & { dueTime?: number }
): Promise<T | K | undefined> {
const get$: Observable<T | K> = this.get$<T, K>(path, options).pipe(filterNil());
const getAsync: Promise<T | K> = firstValueFrom(get$);

if (options?.dueTime != null) {
const dueAsync: Promise<undefined> = delayedPromise(undefined, options.dueTime);
Expand All @@ -142,42 +144,48 @@ export class DefaultEnvironmentQuery implements EnvironmentQuery {
return getAsync;
}

get<T = Property>(path: Path, options?: GetOptions<T>): T | undefined {
get<T extends Property, K = T>(path: Path, options?: GetOptions<T, K>): T | K | undefined {
const state: EnvironmentState = this.getAll();
let property: GetProperty<T> = this.getProperty(state, path);
let property: unknown = this.getProperty(state, path);

property = this.getDefaultValue(property, options?.defaultValue);
property = this.getTargetType(property, options?.targetType);
property = this.getDefaultValue<T>(property, options?.defaultValue);
property = this.getTargetType<T, K>(property, options?.targetType);
property = this.getTranspile(property, options?.transpile, options?.config);

return property as T;
if (options?.required && property === undefined) {
throw new ReferenceError(`The environment property "${path}" is not defined`);
}

return property as T | K | undefined;
}

protected getProperty(state: EnvironmentState, path: Path): Property | undefined {
protected getProperty(state: EnvironmentState, path: Path): unknown {
return get(state, path);
}

protected getDefaultValue(property?: Property, defaultValue?: Property): Property | undefined {
return property === undefined && defaultValue !== undefined ? defaultValue : property;
protected getDefaultValue<T>(property?: unknown, defaultValue?: T): T | undefined {
return property === undefined && defaultValue !== undefined ? defaultValue : (property as T | undefined);
}

protected getTargetType<T>(property?: Property, targetType?: (property: Property) => T): GetProperty<T> {
return property !== undefined && targetType !== undefined ? targetType(property) : property;
protected getTargetType<T, K>(property?: unknown, targetType?: (property?: T) => K): T | K | undefined {
return property !== undefined && targetType !== undefined ? targetType(property as T) : (property as T | undefined);
}

protected getTranspile<T>(
property?: Property | T,
property?: unknown,
transpile?: EnvironmentState,
config?: EnvironmentQueryConfig
): GetProperty<T> {
return property !== undefined && transpile !== undefined ? this.transpile(transpile, property, config) : property;
): T | undefined {
return property !== undefined && transpile !== undefined
? (this.transpile(transpile, property, config) as T)
: (property as T | undefined);
}

protected transpile<T>(
transpile: EnvironmentState,
property?: Property | T,
config?: EnvironmentQueryConfig
): GetProperty<T> {
): T | Property {
const localConfig: DeepRequired<EnvironmentQueryConfig> = this.getLocalConfig(config);

if (isString(property)) {
Expand Down
49 changes: 45 additions & 4 deletions packages/environment/src/lib/query/environment-query.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Observable } from 'rxjs';
import { AtLeastOne } from '../helpers';
import { Path } from '../path';
import { EnvironmentState, Property } from '../store';
import { GetOptions } from './get-options.interface';
import { GetOptions, GetOptionsAsync, GetOptionsObs } from './get-options.interface';

/**
* Gets the properties from the environment.
Expand Down Expand Up @@ -72,27 +72,68 @@ export abstract class EnvironmentQuery {
/**
* Gets the environment property at path.
* @template T The expected return type.
* @template K The expected property target type.
* @param path The property path to resolve.
* @param options The options to get a property.
* @returns The distinct environment property at path as Observable.
*/
abstract get$<T = Property>(path: Path, options?: GetOptions<T>): Observable<T | undefined>;
abstract get$<T extends Property, K = T>(
path: Path,
options: GetOptionsObs<T, K> & { targetType: (property?: T) => K }
): Observable<K>;
abstract get$<T extends Property, K = T>(
path: Path,
options: GetOptionsObs<T, K> & { defaultValue: T }
): Observable<T>;
abstract get$<T extends Property, K = T>(path: Path, options?: GetOptionsObs<T, K>): Observable<T | undefined>;

abstract get$<T extends Property, K = T>(path: Path, options?: GetOptionsObs<T, K>): Observable<T | K | undefined>;

/**
* Gets the environment property at path.
* @template T The expected return type.
* @template K The expected property target type.
* @param path The property path to resolve.
* @param options The options to get a property.
* @returns The first non nil environment property at path as Promise.
*/
abstract getAsync<T = Property>(path: Path, options?: GetOptions<T>): Promise<T | undefined>;

abstract getAsync<T extends Property, K = T>(
path: Path,
options: GetOptionsAsync<T, K> & { targetType: (property?: T) => K } & { dueTime: number }
): Promise<K | undefined>;
abstract getAsync<T extends Property, K = T>(
path: Path,
options: GetOptionsAsync<T, K> & { targetType: (property?: T) => K }
): Promise<K>;
abstract getAsync<T extends Property, K = T>(
path: Path,
options: GetOptionsAsync<T, K> & { dueTime: number }
): Promise<T | undefined>;
abstract getAsync<T extends Property, K = T>(path: Path, options?: GetOptionsAsync<T, K>): Promise<T>;

abstract getAsync<T extends Property, K = T>(path: Path, options?: GetOptionsAsync<T, K>): Promise<T | K | undefined>;

/**
* Gets the environment property at path.
* @template T The expected return type.
* @template K The expected property target type.
* @param path The property path to resolve.
* @param options The options to get a property.
* @returns The environment property at path.
*/
abstract get<T = Property>(path: Path, options?: GetOptions<T>): T | undefined;
abstract get<T extends Property, K = T>(
path: Path,
options: GetOptions<T, K> & { targetType: (property?: T) => K }
): K;
abstract get<T extends Property, K = T>(
path: Path,
options: GetOptions<T, K> & ({ defaultValue: T } | { required: true })
): T;
abstract get<T extends Property, K = T>(path: Path, options?: GetOptions<T, K>): T | undefined;

abstract get<T extends Property, K = T>(
path: Path,
options?: GetOptions<T, K> & { required?: boolean }
): T | K | undefined;
}
38 changes: 27 additions & 11 deletions packages/environment/src/lib/query/get-options.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,51 @@ import { EnvironmentState, Property } from '../store';
import { EnvironmentQueryConfig } from './environment-query-config.interface';

/**
* The options to get a property.
* The options to get a property as Observable.
*/
export interface GetOptions<T> {
export interface GetOptionsObs<T extends Property, K = T> {
/**
* The default value to resolve if no value is found.
* The default value to resolve if value is undefined.
* @template T The property type.
*/
defaultValue?: Property;
defaultValue?: T;

/**
* Converts the returned value.
* @template T The expected property target type.
* @param property The value of the property at path.
* Converts the returned value to the target type.
* @template T The property type.
* @template K The expected property target type.
* @param value The value of the property at path.
* @returns The converted value.
*/
targetType?: (property: Property) => T;
targetType?: (value?: T) => K;

/**
* The properties to resolve the interpolation.
* Properties to resolve the interpolation.
*/
transpile?: EnvironmentState;

/**
* The custom query config for this check.
* Custom query config for this operation.
*/
config?: EnvironmentQueryConfig;
}

/**
* The options to get a property as Promise.
*/
export interface GetOptionsAsync<T extends Property, K = T> extends GetOptionsObs<T, K> {
/**
* The maximum waiting time before emit undefined.
* Only used by the Async operation.
*/
dueTime?: number;
}

/**
* The options to get a property.
*/
export interface GetOptions<T extends Property, K = T> extends GetOptionsObs<T, K> {
/**
* If true, the query throws error if the property doesn't exist.
*/
required?: boolean;
}

0 comments on commit 38e86f9

Please sign in to comment.