Skip to content
This repository has been archived by the owner on Dec 11, 2023. It is now read-only.

Commit

Permalink
[feature] sugar (#7)
Browse files Browse the repository at this point in the history
* Add object/isSugarObject

* Add SugarFormError

* Add SugarEventEmitter

* Add Empty Sugar

* Add Logger

* Refresh Export

* Add getter and setter

* Add isDirty

* Follow EsLint
  • Loading branch information
AsPulse authored Mar 11, 2023
1 parent 521c36d commit 77c3812
Show file tree
Hide file tree
Showing 18 changed files with 1,380 additions and 19 deletions.
2 changes: 1 addition & 1 deletion jest.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ module.exports = {
transform: {
'^.+\\.tsx?$': '@swc/jest',
},
testEnvironment: 'node',
testEnvironment: 'jsdom',
};
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@rollup/plugin-commonjs": "^24.0.1",
"@swc/core": "^1.3.37",
"@swc/jest": "^0.2.24",
"@testing-library/react": "^14.0.0",
"@types/jest": "^29.4.0",
"@types/react": "^18.0.28",
"@typescript-eslint/eslint-plugin": "^5.54.0",
Expand All @@ -26,6 +27,7 @@
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-unused-imports": "^2.0.0",
"jest": "^29.4.3",
"jest-environment-jsdom": "^29.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rollup": "^3.18.0",
Expand Down
35 changes: 35 additions & 0 deletions src/component/sugar/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { Sugar, SugarObjectNode, SugarUser, SugarUserReshaper } from '.';
import { SugarDownstreamEventEmitter } from '../../util/events/downstreamEvent';
import { SugarUpstreamEventEmitter } from '../../util/events/upstreamEvent';
import type { SugarObject } from '../../util/object';
import { isSugarObject } from '../../util/object';
import { useSugar } from './use';

export function createEmptySugar<T>(path: string, template: T): Sugar<T> {
const sugar: Sugar<T> = {
path,
mounted: false,
template,
upstream: new SugarUpstreamEventEmitter(),
downstream: new SugarDownstreamEventEmitter(),
use:
<U extends SugarObject>(options: SugarUserReshaper<T, U>) => useSugar<T, U>(sugar, options),
};

if (isSugarObject(template)) {
(sugar as Sugar<SugarObject>).useObject =
(options: SugarUser): SugarObjectNode<SugarObject> =>
useSugar<SugarObject, SugarObject>(
sugar as Sugar<SugarObject>,
{
...options,
reshape: {
transform: x => x,
deform: x => x,
},
},
);
}

return sugar;
}
12 changes: 12 additions & 0 deletions src/component/sugar/dirty.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { Sugar } from '.';
import { debug } from '../../util/logger';

export function setDirty<T>(sugar: Sugar<T>, isDirty: boolean): void {
if (!sugar.mounted) {
debug('WARN', `Sugar is not mounted when tried to set dirty. Path: ${sugar.path}`);
return;
}
if (sugar.isDirty === isDirty) return;
sugar.isDirty = isDirty;
sugar.upstream.fire('updateDirty', { isDirty });
}
64 changes: 64 additions & 0 deletions src/component/sugar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/* eslint-disable @typescript-eslint/no-empty-interface */
import type { SugarDownstreamEventEmitter } from '../../util/events/downstreamEvent';
import type { SugarUpstreamEventEmitter } from '../../util/events/upstreamEvent';
import type { SugarObject } from '../../util/object';

export type Sugar<T> = SugarData<T> & ({
mounted: false,
} | {
mounted: true,
get: () => SugarValue<T>,
set: (value: T) => void,
isDirty: boolean,
});

export type SugarData<T> = {
path: string,
template: T,
upstream: SugarUpstreamEventEmitter,
downstream: SugarDownstreamEventEmitter,
} & (
T extends SugarObject ?
{
use: <U extends SugarObject>(options: SugarUserReshaper<T, U>) => SugarObjectNode<U>,
useObject: (options: SugarUser) => SugarObjectNode<T>
} : {
use: <U extends SugarObject>(options: SugarUserReshaper<T, U>) => SugarObjectNode<U>,
}
);
// ) & (
// T extends Array<infer U> ?
// { useArray: (options: SugarUserArray<U>) => SugarArrayNode<U> } :
// Record<string, never>
// );

export type SugarValue<T> = {
success: true,
value: T,
} | {
success: false,
value: unknown,
};

export interface SugarUser {

}

export interface SugarUserReshaper<T, U extends SugarObject> extends SugarUser {
reshape: {
transform: (value: U) => T,
deform: (value: T) => U,
}
}

export interface SugarObjectNode<U extends SugarObject> {
fields: { [K in keyof U]: Sugar<U[K]> },
}

// export interface SugarUserArray<T> {
//
// }
//
// export interface SugarArrayNode<T> {
//
// }
135 changes: 135 additions & 0 deletions src/component/sugar/use.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import type { MutableRefObject } from 'react';
import { useRef } from 'react';
import type { Sugar, SugarUserReshaper, SugarObjectNode, SugarValue } from '.';
import { SugarFormError } from '../../util/error';
import { debug } from '../../util/logger';
import type { BetterObjectConstructor, SugarObject } from '../../util/object';
import { createEmptySugar } from './create';
import { setDirty } from './dirty';

declare const Object: BetterObjectConstructor;

export function useSugar<T, U extends SugarObject>(
sugar: Sugar<T>,
options: SugarUserReshaper<T, U>,
): SugarObjectNode<U> {

const fieldsRef = useRef<SugarObjectNode<U>['fields']>();
let fields = fieldsRef.current;

if (sugar.mounted && fields === undefined) {
debug('WARN', 'Sugar is already mounted, but fields are not initialized. Remounting... Path: ${sugar.path}');
}

if (sugar.mounted && fields !== undefined) {
debug('WARN', `Sugar is already mounted. Path: ${sugar.path}`);
} else {
debug('DEBUG', `Mounting sugar. Path: ${sugar.path}`);
const mounted = mountSugar(sugar, options, fieldsRef);
fields = mounted.fields;
fieldsRef.current = fields;
}

return {
fields,
};
}

export function mountSugar<T, U extends SugarObject>(
sugar: Sugar<T>,
options: SugarUserReshaper<T, U>,
fieldsRef: MutableRefObject<SugarObjectNode<U>['fields'] | undefined>,
): SugarObjectNode<U> {
const template = options.reshape.deform(sugar.template);
debug('DEBUG', `Template: ${JSON.stringify(template)}`);

const fields = wrapSugar(sugar.path, template);

const getter = (): SugarValue<T> => {
const fields = fieldsRef.current;
if (fields === undefined) throw new SugarFormError('SF0021', `Path: ${sugar.path}}`);
const value = get<U>(fields);
debug('DEBUG', `Getting Value of Sugar: ${JSON.stringify(value)}, Path: ${sugar.path}`);
return !value.success ? value : {
success: true,
value: options.reshape.transform(value.value),
};
};

const setter = (value: T): void => {
debug('DEBUG', `Setting value of sugar. Path: ${sugar.path}`);
const fields = fieldsRef.current;
if (fields === undefined) throw new SugarFormError('SF0021', `Path: ${sugar.path}}`);
set<U>(fields, options.reshape.deform(value));
};

const dirtyControl = ({ isDirty }: { isDirty: boolean }) : void => {
const fields = fieldsRef.current;
if (!sugar.mounted || fields === undefined) throw new SugarFormError('SF0021', `Path: ${sugar.path}}`);
if (isDirty) {
if (sugar.isDirty) return;
setDirty(sugar, true);
} else {
if (Object.values(fields).some(s => s.mounted && s.isDirty)) return;
setDirty(sugar, false);
}
};

Object.values(fields).forEach(sugar => sugar.upstream.listen('updateDirty', dirtyControl));

const updateSugar = sugar as Sugar<T> & { mounted: true };
updateSugar.mounted = true;
updateSugar.get = getter;
updateSugar.set = setter;
updateSugar.isDirty = false;

return { fields };
}


export function wrapSugar<T extends SugarObject>(path: string, template: T): SugarObjectNode<T>['fields'] {
const fields: SugarObjectNode<T>['fields'] = {} as SugarObjectNode<T>['fields'];

for (const key in template) {
fields[key] = createEmptySugar(`${path}.${key}`, template[key]);
}

return fields;
}

export function get<T extends SugarObject>(fields: SugarObjectNode<T>['fields']): SugarValue<T> {
const result = {} as { [P in keyof T]: unknown };
let success = true;

for (const key in fields) {
const sugar = fields[key];
if (!sugar.mounted) {
debug('WARN', `Sugar is not mounted when tried to get. Path: ${sugar.path}`);
result[key] = null;
success = false;
} else {
const value = sugar.get();
result[key] = value.value;
success &&= value.success;
}
}

return success ? {
success,
value: result as T,
} : {
success,
value: result,
};
}

export function set<T extends SugarObject>(fields: SugarObjectNode<T>['fields'], value: T): void {
for (const key in fields) {
const sugar = fields[key];
if (!sugar.mounted) {
debug('WARN', `Sugar is not mounted when tried to set. Path: ${sugar.path}`);
} else {
sugar.set(value[key]);
}
}
}
6 changes: 2 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
export function helloSugarForm(): void {
console.log('Hello, Sugar Form!');
return;
}
export type { Sugar } from './component/sugar';
export { setSugarFormLogLevel } from './util/logger';
35 changes: 35 additions & 0 deletions src/util/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const SugarFormErrorMap = [
{
code: 'SF0001',
name: 'TypeMismatch',
message: 'Incorrect type detected.\nThis may be a bug in SugarForm itself. It would be helpful if you could post the stack trace to GitHub.',
},
{
code: 'SF0002',
name: 'DifferentFromTypeDefinition',
message: 'Incorrect type detected.\nIt is believed that the code does not follow TypeScript type annotations.',
},
{
code: 'SF0011',
name: 'Not Implemented',
message: 'This feature is not implemented yet.',
},
{
code: 'SF0021',
name: 'RequestValueToUnmoutedSugar',
message: 'Sugar was forced unmounted from outside.',
},
] as const;

export class SugarFormError extends Error {
constructor(id: (typeof SugarFormErrorMap)[number]['code'], message?: string) {
const error = SugarFormErrorMap.find(e => e.code === id) ?? {
code: 'SF9999',
name: 'SugarFormUnknownError',
message: 'Unknown error occured.',
};
super(`${error.code} (${error.name})\n${error.message}${message !== undefined ? `\ninfo for debug: ${message}` : ''}`);
this.name = new.target.name;
Error.captureStackTrace(this, this.constructor);
}
}
27 changes: 27 additions & 0 deletions src/util/event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { SugarFormError } from './error';

export class SugarEventEmitter<EventTable extends Record<string, Record<string, unknown>>> {
private listeners: Partial<
Record<keyof EventTable, Array<(param: EventTable[keyof EventTable]) => void>>
> = {};


public listen<K extends keyof EventTable>(
eventName: K, callback: (param: EventTable[K]) => void,
): void {
if (!this.listeners[eventName]) {
this.listeners[eventName] = [];
}
const listener: Array<(param: EventTable[K]) => void> | undefined = this.listeners[eventName];
if (listener === undefined) {
throw new SugarFormError('SF0001', 'SugarEventEmitter#listen');
}
listener.push(callback);
}

public fire<K extends keyof EventTable>(eventName: K, param: EventTable[K]): void {
const listener: Array<(param: EventTable[K]) => void> | undefined = this.listeners[eventName];
if (listener === undefined) return;
listener.forEach(l => l(param));
}
}
3 changes: 3 additions & 0 deletions src/util/events/downstreamEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { SugarEventEmitter } from '../event';

export class SugarDownstreamEventEmitter extends SugarEventEmitter<Record<string, never>> {}
6 changes: 6 additions & 0 deletions src/util/events/upstreamEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { SugarEventEmitter } from '../event';

export class SugarUpstreamEventEmitter extends SugarEventEmitter<{
updateDirty: { isDirty: boolean },
mounted: Record<string, never>,
}> {}
24 changes: 24 additions & 0 deletions src/util/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/* eslint-disable no-console */
type LogLevel = 'DEBUG' | 'INFO' | 'WARN' | 'SILENT';
let SugarFormLogLevel: LogLevel = 'SILENT';
export function setSugarFormLogLevel(level: LogLevel): void {
SugarFormLogLevel = level;
}

export function debug(level: LogLevel, message: string): void {
if (SugarFormLogLevel === 'SILENT') return;
if (level === 'WARN') {
console.warn(message);
return;
}
if (SugarFormLogLevel === 'WARN') return;
if (level === 'INFO') {
console.info(message);
return;
}
if (SugarFormLogLevel === 'INFO') return;
if (level === 'DEBUG') {
console.debug(message);
return;
}
}
8 changes: 8 additions & 0 deletions src/util/object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export type SugarObject = Record<string, unknown>;
export function isSugarObject(obj: unknown): obj is SugarObject {
return obj?.constructor.name === 'Object';
}

export declare interface BetterObjectConstructor extends ObjectConstructor {
values<T>(obj: T): Array<T[keyof T]>;
}
Loading

0 comments on commit 77c3812

Please sign in to comment.