Skip to content

Commit

Permalink
Merge pull request #3276 from D4N14L/user/danade/AllowCustomMergeBeha…
Browse files Browse the repository at this point in the history
…vior

[heft-config-file] Add inline `inheritanceType` specification in configuration files
  • Loading branch information
D4N14L authored Mar 19, 2022
2 parents f65e9ae + 175475b commit b03815b
Show file tree
Hide file tree
Showing 16 changed files with 1,031 additions and 132 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@rushstack/heft-config-file",
"comment": "Add support for an inline \"$<propertyName>.inheritanceType\" property. This feature allows for configuration files to specify how object and array properties are inherited, overriding the default inheritance behavior provided by the configuration file class.",
"type": "minor"
}
],
"packageName": "@rushstack/heft-config-file"
}
5 changes: 3 additions & 2 deletions common/reviews/api/heft-config-file.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,21 @@ export interface IJsonPathsMetadata {
export enum InheritanceType {
append = "append",
custom = "custom",
merge = "merge",
replace = "replace"
}

// @beta (undocumented)
export interface IOriginalValueOptions<TParentProperty> {
// (undocumented)
parentObject: TParentProperty;
parentObject: Partial<TParentProperty>;
// (undocumented)
propertyName: keyof TParentProperty;
}

// @beta (undocumented)
export type IPropertiesInheritance<TConfigurationFile> = {
[propertyName in keyof TConfigurationFile]?: IPropertyInheritance<InheritanceType.append | InheritanceType.replace> | ICustomPropertyInheritance<TConfigurationFile[propertyName]>;
[propertyName in keyof TConfigurationFile]?: IPropertyInheritance<InheritanceType.append | InheritanceType.merge | InheritanceType.replace> | ICustomPropertyInheritance<TConfigurationFile[propertyName]>;
};

// @beta (undocumented)
Expand Down
383 changes: 254 additions & 129 deletions libraries/heft-config-file/src/ConfigurationFile.ts

Large diffs are not rendered by default.

273 changes: 273 additions & 0 deletions libraries/heft-config-file/src/test/ConfigurationFile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,279 @@ describe(ConfigurationFile.name, () => {
});
});

describe('a complex file with inheritance type annotations', () => {
interface IInheritanceTypeConfigFile {
a: string;
b: { c: string }[];
d: {
e: string;
f: string;
g: { h: string }[];
i: { j: string }[];
k: {
l: string;
m: { n: string }[];
z?: string;
};
o: {
p: { q: string }[];
};
r: {
s: string;
};
y?: {
z: string;
};
};
y?: {
z: string;
};
}

interface ISimpleInheritanceTypeConfigFile {
a: { b: string }[];
c: {
d: { e: string }[];
};
f: {
g: { h: string }[];
i: {
j: { k: string }[];
};
};
l: string;
}

it('Correctly loads a complex config file with inheritance type annotations', async () => {
const projectRelativeFilePath: string = 'inheritanceTypeConfigFile/inheritanceTypeConfigFileB.json';
const rootConfigFilePath: string = nodeJsPath.resolve(
__dirname,
'inheritanceTypeConfigFile',
'inheritanceTypeConfigFileA.json'
);
const secondConfigFilePath: string = nodeJsPath.resolve(
__dirname,
'inheritanceTypeConfigFile',
'inheritanceTypeConfigFileB.json'
);
const schemaPath: string = nodeJsPath.resolve(
__dirname,
'inheritanceTypeConfigFile',
'inheritanceTypeConfigFile.schema.json'
);

const configFileLoader: ConfigurationFile<IInheritanceTypeConfigFile> =
new ConfigurationFile<IInheritanceTypeConfigFile>({
projectRelativeFilePath: projectRelativeFilePath,
jsonSchemaPath: schemaPath
});
const loadedConfigFile: IInheritanceTypeConfigFile =
await configFileLoader.loadConfigurationFileForProjectAsync(terminal, __dirname);
const expectedConfigFile: IInheritanceTypeConfigFile = {
a: 'A',
// "$b.inheritanceType": "append"
b: [{ c: 'A' }, { c: 'B' }],
// "$d.inheritanceType": "merge"
d: {
e: 'A',
f: 'B',
// "$g.inheritanceType": "append"
g: [{ h: 'A' }, { h: 'B' }],
// "$i.inheritanceType": "replace"
i: [{ j: 'B' }],
// "$k.inheritanceType": "merge"
k: {
l: 'A',
m: [{ n: 'A' }, { n: 'B' }],
z: 'B'
},
// "$o.inheritanceType": "replace"
o: {
p: [{ q: 'B' }]
},
r: {
s: 'A'
},
y: {
z: 'B'
}
},
y: {
z: 'B'
}
};

expect(JSON.stringify(loadedConfigFile)).toEqual(JSON.stringify(expectedConfigFile));

expect(configFileLoader.getObjectSourceFilePath(loadedConfigFile.b[0])).toEqual(rootConfigFilePath);
expect(configFileLoader.getObjectSourceFilePath(loadedConfigFile.b[1])).toEqual(secondConfigFilePath);

// loadedConfigFile.d source path is the second config file since it was merged into the first
expect(configFileLoader.getObjectSourceFilePath(loadedConfigFile.d)).toEqual(secondConfigFilePath);
expect(configFileLoader.getObjectSourceFilePath(loadedConfigFile.d.g[0])).toEqual(rootConfigFilePath);
expect(configFileLoader.getObjectSourceFilePath(loadedConfigFile.d.g[1])).toEqual(secondConfigFilePath);
expect(configFileLoader.getObjectSourceFilePath(loadedConfigFile.d.i[0])).toEqual(secondConfigFilePath);

// loadedConfigFile.d.k source path is the second config file since it was merged into the first
expect(configFileLoader.getObjectSourceFilePath(loadedConfigFile.d.k)).toEqual(secondConfigFilePath);
expect(configFileLoader.getObjectSourceFilePath(loadedConfigFile.d.k.m[0])).toEqual(rootConfigFilePath);
expect(configFileLoader.getObjectSourceFilePath(loadedConfigFile.d.k.m[1])).toEqual(
secondConfigFilePath
);

// loadedConfigFile.d.o source path is the second config file since it replaced the first
expect(configFileLoader.getObjectSourceFilePath(loadedConfigFile.d.o)).toEqual(secondConfigFilePath);
expect(configFileLoader.getObjectSourceFilePath(loadedConfigFile.d.o.p[0])).toEqual(
secondConfigFilePath
);

expect(configFileLoader.getObjectSourceFilePath(loadedConfigFile.d.r)).toEqual(rootConfigFilePath);

expect(configFileLoader.getObjectSourceFilePath(loadedConfigFile.d.y!)).toEqual(secondConfigFilePath);

expect(configFileLoader.getObjectSourceFilePath(loadedConfigFile.y!)).toEqual(secondConfigFilePath);
});

it('Correctly loads a complex config file with a single inheritance type annotation', async () => {
const projectRelativeFilePath: string =
'simpleInheritanceTypeConfigFile/simpleInheritanceTypeConfigFileB.json';
const rootConfigFilePath: string = nodeJsPath.resolve(
__dirname,
'simpleInheritanceTypeConfigFile',
'simpleInheritanceTypeConfigFileA.json'
);
const secondConfigFilePath: string = nodeJsPath.resolve(
__dirname,
'simpleInheritanceTypeConfigFile',
'simpleInheritanceTypeConfigFileB.json'
);
const schemaPath: string = nodeJsPath.resolve(
__dirname,
'simpleInheritanceTypeConfigFile',
'simpleInheritanceTypeConfigFile.schema.json'
);

const configFileLoader: ConfigurationFile<ISimpleInheritanceTypeConfigFile> =
new ConfigurationFile<ISimpleInheritanceTypeConfigFile>({
projectRelativeFilePath: projectRelativeFilePath,
jsonSchemaPath: schemaPath
});
const loadedConfigFile: ISimpleInheritanceTypeConfigFile =
await configFileLoader.loadConfigurationFileForProjectAsync(terminal, __dirname);
const expectedConfigFile: ISimpleInheritanceTypeConfigFile = {
a: [{ b: 'A' }, { b: 'B' }],
c: {
d: [{ e: 'B' }]
},
// "$f.inheritanceType": "merge"
f: {
g: [{ h: 'A' }, { h: 'B' }],
i: {
j: [{ k: 'B' }]
}
},
l: 'A'
};

expect(JSON.stringify(loadedConfigFile)).toEqual(JSON.stringify(expectedConfigFile));

expect(configFileLoader.getObjectSourceFilePath(loadedConfigFile.a[0])).toEqual(rootConfigFilePath);
expect(configFileLoader.getObjectSourceFilePath(loadedConfigFile.a[1])).toEqual(secondConfigFilePath);

expect(configFileLoader.getObjectSourceFilePath(loadedConfigFile.c)).toEqual(secondConfigFilePath);
expect(configFileLoader.getObjectSourceFilePath(loadedConfigFile.c.d[0])).toEqual(secondConfigFilePath);

// loadedConfigFile.f source path is the second config file since it was merged into the first
expect(configFileLoader.getObjectSourceFilePath(loadedConfigFile.f)).toEqual(secondConfigFilePath);
expect(configFileLoader.getObjectSourceFilePath(loadedConfigFile.f.g[0])).toEqual(rootConfigFilePath);
expect(configFileLoader.getObjectSourceFilePath(loadedConfigFile.f.g[1])).toEqual(secondConfigFilePath);
expect(configFileLoader.getObjectSourceFilePath(loadedConfigFile.f.i)).toEqual(secondConfigFilePath);
expect(configFileLoader.getObjectSourceFilePath(loadedConfigFile.f.i.j[0])).toEqual(
secondConfigFilePath
);
});

it("throws an error when an array uses the 'merge' inheritance type", async () => {
const schemaPath: string = nodeJsPath.resolve(
__dirname,
'simpleInheritanceTypeConfigFile',
'simpleInheritanceTypeConfigFile.schema.json'
);
const configFileLoader: ConfigurationFile<void> = new ConfigurationFile({
projectRelativeFilePath: 'simpleInheritanceTypeConfigFile/badInheritanceTypeConfigFileA.json',
jsonSchemaPath: schemaPath
});

await expect(
configFileLoader.loadConfigurationFileForProjectAsync(terminal, __dirname)
).rejects.toThrowErrorMatchingSnapshot();
});

it("throws an error when a keyed object uses the 'append' inheritance type", async () => {
const schemaPath: string = nodeJsPath.resolve(
__dirname,
'simpleInheritanceTypeConfigFile',
'simpleInheritanceTypeConfigFile.schema.json'
);
const configFileLoader: ConfigurationFile<void> = new ConfigurationFile({
projectRelativeFilePath: 'simpleInheritanceTypeConfigFile/badInheritanceTypeConfigFileB.json',
jsonSchemaPath: schemaPath
});

await expect(
configFileLoader.loadConfigurationFileForProjectAsync(terminal, __dirname)
).rejects.toThrowErrorMatchingSnapshot();
});

it('throws an error when a non-object property uses an inheritance type', async () => {
const schemaPath: string = nodeJsPath.resolve(
__dirname,
'simpleInheritanceTypeConfigFile',
'simpleInheritanceTypeConfigFile.schema.json'
);
const configFileLoader: ConfigurationFile<void> = new ConfigurationFile({
projectRelativeFilePath: 'simpleInheritanceTypeConfigFile/badInheritanceTypeConfigFileC.json',
jsonSchemaPath: schemaPath
});

await expect(
configFileLoader.loadConfigurationFileForProjectAsync(terminal, __dirname)
).rejects.toThrowErrorMatchingSnapshot();
});

it('throws an error when an inheritance type is specified for an unspecified property', async () => {
const schemaPath: string = nodeJsPath.resolve(
__dirname,
'simpleInheritanceTypeConfigFile',
'simpleInheritanceTypeConfigFile.schema.json'
);
const configFileLoader: ConfigurationFile<void> = new ConfigurationFile({
projectRelativeFilePath: 'simpleInheritanceTypeConfigFile/badInheritanceTypeConfigFileD.json',
jsonSchemaPath: schemaPath
});

await expect(
configFileLoader.loadConfigurationFileForProjectAsync(terminal, __dirname)
).rejects.toThrowErrorMatchingSnapshot();
});

it('throws an error when an unsupported inheritance type is specified', async () => {
const schemaPath: string = nodeJsPath.resolve(
__dirname,
'simpleInheritanceTypeConfigFile',
'simpleInheritanceTypeConfigFile.schema.json'
);
const configFileLoader: ConfigurationFile<void> = new ConfigurationFile({
projectRelativeFilePath: 'simpleInheritanceTypeConfigFile/badInheritanceTypeConfigFileE.json',
jsonSchemaPath: schemaPath
});

await expect(
configFileLoader.loadConfigurationFileForProjectAsync(terminal, __dirname)
).rejects.toThrowErrorMatchingSnapshot();
});
});

describe('loading a rig', () => {
const projectFolder: string = nodeJsPath.resolve(__dirname, 'project-referencing-rig');
const rigConfig: RigConfig = RigConfig.loadForProjectFolder({ projectFolderPath: projectFolder });
Expand Down
Loading

0 comments on commit b03815b

Please sign in to comment.