Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide additional tester context #1975

Merged
merged 2 commits into from
Jul 15, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,21 @@ Therefore JSON Forms was not able to properly run the testers on schemas contain
The workaround for this was to resolve the JSON Schema by hand before handing it over to JSON Forms.
Only the React renderers did this automatically but we removed this functionality, see the next section for more information.

We now added an additional parameter to the testers, the `rootSchema`.
We now added an additional parameter to the testers, the new `TesterContext`.

```ts
type Tester = (uischema: UISchemaElement, schema: JsonSchema, rootSchema: JsonSchema) => boolean;
type RankedTester = (uischema: UISchemaElement, schema: JsonSchema, rootSchema: JsonSchema) => number;
interface TesterContext {
rootSchema: JsonSchema;
config: any;
}

type Tester = (uischema: UISchemaElement, schema: JsonSchema, context: TesterContext) => boolean;
type RankedTester = (uischema: UISchemaElement, schema: JsonSchema, context: TesterContext) => number;
```

This allows the testers to resolve any `$ref` they might encounter in their handed over `schema`.
This allows the testers to resolve any `$ref` they might encounter in their handed over `schema` by using the context's `rootSchema`.
Therefore the manual resolving of JSON Schemas before handing them over to JSON Forms does not need to be performed in those cases.
In addition, testers can now access the global `config` to consider default UI Schema options.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are also other use cases so I would just add that the config is now available. Especially as we don't use the config in this PR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of global I usually use the term "form wide" to avoid confusion


### Removal of JSON Schema $Ref Parser

Expand Down Expand Up @@ -114,6 +120,10 @@ The utility function `fromScopable` was renamed to `fromScoped` accordingly.

Date Picker in Angular Material will use the global configuration of your Angular Material application.

### React prop mapping functions

Renamed `ctxToJsonFormsDispatchProps` to `ctxToJsonFormsRendererProps` in order to better reflect the function's purpose.

## Migrating to JSON Forms 2.5

### JsonForms Component for Angular
Expand Down
6 changes: 5 additions & 1 deletion packages/angular-material/src/controls/number.renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,11 @@ export class NumberControlRenderer extends JsonFormsControl {

mapAdditionalProps(props:StatePropsOfControl) {
if (this.scopedSchema) {
const defaultStep = isNumberControl(this.uischema, this.rootSchema, this.rootSchema)
const testerContext = {
rootSchema: this.rootSchema,
config: props.config
}
const defaultStep = isNumberControl(this.uischema, this.rootSchema, testerContext)
? 0.1
: 1;
this.min = this.scopedSchema.minimum;
Expand Down
3 changes: 2 additions & 1 deletion packages/angular-material/test/date-control.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { Actions, ControlElement, JsonSchema } from '@jsonforms/core';
import { DateControlRenderer, DateControlRendererTester } from '../src';
import { FlexLayoutModule } from '@angular/flex-layout';
import { JsonFormsAngularService } from '@jsonforms/angular';
import { createTesterContext } from './util';

const data = { foo: '2018-01-01' };
const schema: JsonSchema = {
Expand All @@ -59,7 +60,7 @@ const uischema: ControlElement = {

describe('Material boolean field tester', () => {
it('should succeed', () => {
expect(DateControlRendererTester(uischema, schema, schema)).toBe(2);
expect(DateControlRendererTester(uischema, schema, createTesterContext(schema))).toBe(2);
});
});
const imports = [
Expand Down
9 changes: 5 additions & 4 deletions packages/angular-material/test/table-control.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
} from '../src/other/table.renderer';
import { FlexLayoutModule } from '@angular/flex-layout';
import { setupMockStore } from '@jsonforms/angular-test';
import { createTesterContext } from './util';

const uischema1: ControlElement = { type: 'Control', scope: '#' };
const uischema2: ControlElement = {
Expand Down Expand Up @@ -95,10 +96,10 @@ const renderers = [

describe('Table tester', () => {
it('should succeed', () => {
expect(TableRendererTester(uischema1, schema_object1, schema_object1)).toBe(3);
expect(TableRendererTester(uischema1, schema_simple1, schema_simple1)).toBe(3);
expect(TableRendererTester(uischema2, schema_object2, schema_object2)).toBe(3);
expect(TableRendererTester(uischema2, schema_simple2, schema_simple2)).toBe(3);
expect(TableRendererTester(uischema1, schema_object1, createTesterContext(schema_object1))).toBe(3);
expect(TableRendererTester(uischema1, schema_simple1, createTesterContext(schema_simple1))).toBe(3);
expect(TableRendererTester(uischema2, schema_object2, createTesterContext(schema_object2))).toBe(3);
expect(TableRendererTester(uischema2, schema_simple2, createTesterContext(schema_simple2))).toBe(3);
});
});
describe('Table', () => {
Expand Down
28 changes: 28 additions & 0 deletions packages/angular-material/test/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
The MIT License

Copyright (c) 2022 EclipseSource
https://github.com/eclipsesource/jsonforms

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
import { JsonSchema, TesterContext } from '@jsonforms/core';

export const createTesterContext =
(rootSchema: JsonSchema, config?: any): TesterContext => ({ rootSchema, config });
10 changes: 7 additions & 3 deletions packages/angular/src/jsonforms.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
import {
createId,
isControl,
getConfig,
JsonFormsProps,
JsonFormsState,
JsonSchema,
Expand Down Expand Up @@ -104,11 +105,14 @@ export class JsonFormsOutlet extends JsonFormsBaseRenderer<UISchemaElement>
const { renderers } = props as JsonFormsProps;
const schema: JsonSchema = this.schema || props.schema;
const uischema = this.uischema || props.uischema;
const rootSchema = props.rootSchema;
const testerContext = {
rootSchema: props.rootSchema,
config: getConfig(state)
};

const renderer = maxBy(renderers, r => r.tester(uischema, schema, rootSchema));
const renderer = maxBy(renderers, r => r.tester(uischema, schema, testerContext));
let bestComponent: Type<any> = UnknownRenderer;
if (renderer !== undefined && renderer.tester(uischema, schema, rootSchema) !== -1) {
if (renderer !== undefined && renderer.tester(uischema, schema, testerContext) !== -1) {
bestComponent = renderer.renderer;
}

Expand Down
56 changes: 33 additions & 23 deletions packages/core/src/testers/testers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,27 @@ export const NOT_APPLICABLE = -1;
* A tester is a function that receives an UI schema and a JSON schema and returns a boolean.
* The rootSchema is handed over as context. Can be used to resolve references.
*/
export type Tester = (uischema: UISchemaElement, schema: JsonSchema, rootSchema: JsonSchema) => boolean;
export type Tester = (uischema: UISchemaElement, schema: JsonSchema, context: TesterContext) => boolean;

/**
* A ranked tester associates a tester with a number.
*/
export type RankedTester = (
uischema: UISchemaElement,
schema: JsonSchema,
rootSchema: JsonSchema
context: TesterContext
) => number;

/**
* Additional context given to a tester in addition to UISchema and JsonSchema.
*/
export interface TesterContext {
/** The root JsonSchema of the form. Can be used to resolve references. */
rootSchema: JsonSchema;
/** The global configuration object given to JsonForms. Can be used to derive default UISchema options. */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

config: any;
}

export const isControl = (uischema: any): uischema is ControlElement =>
!isEmpty(uischema) && uischema.scope !== undefined;

Expand All @@ -75,7 +85,7 @@ export const isControl = (uischema: any): uischema is ControlElement =>
*/
export const schemaMatches = (
predicate: (schema: JsonSchema, rootSchema: JsonSchema) => boolean
): Tester => (uischema: UISchemaElement, schema: JsonSchema, rootSchema: JsonSchema): boolean => {
): Tester => (uischema: UISchemaElement, schema: JsonSchema, context: TesterContext): boolean => {
if (isEmpty(uischema) || !isControl(uischema)) {
return false;
}
Expand All @@ -88,34 +98,34 @@ export const schemaMatches = (
}
let currentDataSchema = schema;
if (hasType(schema, 'object')) {
currentDataSchema = resolveSchema(schema, schemaPath, rootSchema);
currentDataSchema = resolveSchema(schema, schemaPath, context?.rootSchema);
}
if (currentDataSchema === undefined) {
return false;
}

return predicate(currentDataSchema, rootSchema);
return predicate(currentDataSchema, context?.rootSchema);
};

export const schemaSubPathMatches = (
subPath: string,
predicate: (schema: JsonSchema, rootSchema: JsonSchema) => boolean
): Tester => (uischema: UISchemaElement, schema: JsonSchema, rootSchema: JsonSchema): boolean => {
): Tester => (uischema: UISchemaElement, schema: JsonSchema, context: TesterContext): boolean => {
if (isEmpty(uischema) || !isControl(uischema)) {
return false;
}
const schemaPath = uischema.scope;
let currentDataSchema: JsonSchema = schema;
if (hasType(schema, 'object')) {
currentDataSchema = resolveSchema(schema, schemaPath, rootSchema);
currentDataSchema = resolveSchema(schema, schemaPath, context?.rootSchema);
}
currentDataSchema = get(currentDataSchema, subPath);

if (currentDataSchema === undefined) {
return false;
}

return predicate(currentDataSchema, rootSchema);
return predicate(currentDataSchema, context?.rootSchema);
};

/**
Expand Down Expand Up @@ -218,8 +228,8 @@ export const scopeEndIs = (expected: string): Tester => (
export const and = (...testers: Tester[]): Tester => (
uischema: UISchemaElement,
schema: JsonSchema,
rootSchema: JsonSchema
) => testers.reduce((acc, tester) => acc && tester(uischema, schema, rootSchema), true);
context: TesterContext
) => testers.reduce((acc, tester) => acc && tester(uischema, schema, context), true);

/**
* A tester that allow composing other testers by || them.
Expand All @@ -229,8 +239,8 @@ export const and = (...testers: Tester[]): Tester => (
export const or = (...testers: Tester[]): Tester => (
uischema: UISchemaElement,
schema: JsonSchema,
rootSchema: JsonSchema
) => testers.reduce((acc, tester) => acc || tester(uischema, schema, rootSchema), false);
context: TesterContext
) => testers.reduce((acc, tester) => acc || tester(uischema, schema, context), false);
/**
* Create a ranked tester that will associate a number with a given tester, if the
* latter returns true.
Expand All @@ -241,9 +251,9 @@ export const or = (...testers: Tester[]): Tester => (
export const rankWith = (rank: number, tester: Tester) => (
uischema: UISchemaElement,
schema: JsonSchema,
rootSchema: JsonSchema
context: TesterContext
): number => {
if (tester(uischema, schema, rootSchema)) {
if (tester(uischema, schema, context)) {
return rank;
}

Expand All @@ -253,9 +263,9 @@ export const rankWith = (rank: number, tester: Tester) => (
export const withIncreasedRank = (by: number, rankedTester: RankedTester) => (
uischema: UISchemaElement,
schema: JsonSchema,
rootSchema: JsonSchema
context: TesterContext
): number => {
const rank = rankedTester(uischema, schema, rootSchema);
const rank = rankedTester(uischema, schema, context);
if (rank === NOT_APPLICABLE) {
return NOT_APPLICABLE;
}
Expand Down Expand Up @@ -438,13 +448,13 @@ const traverse = (
export const isObjectArrayWithNesting = (
uischema: UISchemaElement,
schema: JsonSchema,
rootSchema: JsonSchema
context: TesterContext
): boolean => {
if (!uiTypeIs('Control')(uischema, schema, rootSchema)) {
if (!uiTypeIs('Control')(uischema, schema, context)) {
return false;
}
const schemaPath = (uischema as ControlElement).scope;
const resolvedSchema = resolveSchema(schema, schemaPath, rootSchema ?? schema);
const resolvedSchema = resolveSchema(schema, schemaPath, context?.rootSchema ?? schema);
let objectDepth = 0;
if (resolvedSchema !== undefined && resolvedSchema.items !== undefined) {
// check if nested arrays
Expand All @@ -466,7 +476,7 @@ export const isObjectArrayWithNesting = (
return true;
}
return false;
}, rootSchema)
}, context?.rootSchema)
) {
return true;
}
Expand Down Expand Up @@ -532,7 +542,7 @@ export const isRangeControl = and(

/**
* Tests whether the given UI schema is of type Control, if the schema
* is of type string and has option format
* is of type integer and has option format
* @type {Tester}
*/
export const isNumberFormatControl = and(
Expand Down Expand Up @@ -566,6 +576,6 @@ export const categorizationHasCategory = (uischema: UISchemaElement) =>
export const not = (tester: Tester): Tester => (
uischema: UISchemaElement,
schema: JsonSchema,
rootSchema: JsonSchema
context: TesterContext

) => !tester(uischema, schema, rootSchema);
) => !tester(uischema, schema, context);
29 changes: 7 additions & 22 deletions packages/core/src/util/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import {
JsonFormsRendererRegistryEntry,
} from '../reducers';
import {
findUISchema,
getAjv,
getCells,
getConfig,
Expand Down Expand Up @@ -889,6 +888,7 @@ export interface OwnPropsOfJsonFormsRenderer extends OwnPropsOfRenderer {}
export interface StatePropsOfJsonFormsRenderer
extends OwnPropsOfJsonFormsRenderer {
rootSchema: JsonSchema;
config: any;
}

export interface JsonFormsProps extends StatePropsOfJsonFormsRenderer {}
Expand All @@ -897,30 +897,15 @@ export const mapStateToJsonFormsRendererProps = (
state: JsonFormsState,
ownProps: OwnPropsOfJsonFormsRenderer
): StatePropsOfJsonFormsRenderer => {
let uischema = ownProps.uischema;
if (uischema === undefined) {
if (ownProps.schema) {
uischema = findUISchema(
state.jsonforms.uischemas,
ownProps.schema,
undefined,
ownProps.path,
undefined,
undefined,
state.jsonforms.core.schema
);
} else {
uischema = getUiSchema(state);
}
}

return {
renderers: ownProps.renderers || get(state.jsonforms, 'renderers') || [],
cells: ownProps.cells || get(state.jsonforms, 'cells') || [],
renderers: ownProps.renderers || get(state.jsonforms, 'renderers'),
cells: ownProps.cells || get(state.jsonforms, 'cells'),
schema: ownProps.schema || getSchema(state),
rootSchema: getSchema(state),
uischema: uischema,
path: ownProps.path
uischema: ownProps.uischema || getUiSchema(state),
path: ownProps.path,
enabled: ownProps.enabled,
config: getConfig(state)
};
};

Expand Down
Loading