Skip to content

Commit

Permalink
Provide additional tester context
Browse files Browse the repository at this point in the history
core:
* Change the third parameter of Tester and RankedTester from just the root schema to a new TesterContext object. It contains the root schema and the global config.
* Remove UI Schema generation from mapStateToJsonFormsRendererProps. Instead, it is taken from the state.

react:
* Rename ctxToJsonFormsDispatchProps to ctxToJsonFormsRendererProps
* Add HOC withJsonFormsRendererProps that injects state props

vue:
* Upgrade @vue/test-utils to the latest stable version
* Add config as an optional renderer prop

Fixes #1970
  • Loading branch information
lucas-koehler committed Jul 12, 2022
1 parent 6a6af7e commit fd93428
Show file tree
Hide file tree
Showing 24 changed files with 227 additions and 140 deletions.
18 changes: 14 additions & 4 deletions MIGRATION.md
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.

### 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
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
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
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
@@ -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
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
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. */
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
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

0 comments on commit fd93428

Please sign in to comment.