Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: initial implementation of
bazel-module
manager (#21893)
- Loading branch information
Showing
15 changed files
with
711 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { BazelDatasource } from '../../datasource/bazel'; | ||
import { ToBazelDep } from './bazel-dep'; | ||
import * as fragments from './fragments'; | ||
|
||
describe('modules/manager/bazel-module/bazel-dep', () => { | ||
describe('ToBazelDep', () => { | ||
it('transforms a record fragment', () => { | ||
const record = fragments.record({ | ||
rule: fragments.string('bazel_dep'), | ||
name: fragments.string('rules_foo'), | ||
version: fragments.string('1.2.3'), | ||
dev_dependency: fragments.boolean(true), | ||
}); | ||
const result = ToBazelDep.parse(record); | ||
expect(result).toEqual({ | ||
datasource: BazelDatasource.id, | ||
depType: 'bazel_dep', | ||
depName: 'rules_foo', | ||
currentValue: '1.2.3', | ||
}); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { z } from 'zod'; | ||
import { BazelDatasource } from '../../datasource/bazel'; | ||
import type { PackageDependency } from '../types'; | ||
import { | ||
BooleanFragmentSchema, | ||
RecordFragmentSchema, | ||
StringFragmentSchema, | ||
} from './fragments'; | ||
|
||
const BazelDepSchema = RecordFragmentSchema.extend({ | ||
children: z.object({ | ||
rule: StringFragmentSchema.extend({ | ||
value: z.literal('bazel_dep'), | ||
}), | ||
name: StringFragmentSchema, | ||
version: StringFragmentSchema, | ||
dev_dependency: BooleanFragmentSchema.optional(), | ||
}), | ||
}); | ||
|
||
export const ToBazelDep = BazelDepSchema.transform( | ||
({ children: { rule, name, version } }): PackageDependency => ({ | ||
datasource: BazelDatasource.id, | ||
depType: rule.value, | ||
depName: name.value, | ||
currentValue: version.value, | ||
}) | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import { Ctx, CtxProcessingError } from './context'; | ||
import * as fragments from './fragments'; | ||
|
||
describe('modules/manager/bazel-module/context', () => { | ||
describe('Ctx', () => { | ||
it('construct simple bazel_dep', () => { | ||
const ctx = new Ctx() | ||
.startRule('bazel_dep') | ||
.startAttribute('name') | ||
.addString('rules_foo') | ||
.startAttribute('version') | ||
.addString('1.2.3') | ||
.endRule(); | ||
|
||
expect(ctx.results).toEqual([ | ||
fragments.record( | ||
{ | ||
rule: fragments.string('bazel_dep'), | ||
name: fragments.string('rules_foo'), | ||
version: fragments.string('1.2.3'), | ||
}, | ||
true | ||
), | ||
]); | ||
}); | ||
|
||
it('construct a rule with array arg', () => { | ||
const ctx = new Ctx() | ||
.startRule('foo_library') | ||
.startAttribute('name') | ||
.addString('my_library') | ||
.startAttribute('srcs') | ||
.startArray() | ||
.addString('first') | ||
.addString('second') | ||
.endArray() | ||
.endRule(); | ||
|
||
expect(ctx.results).toEqual([ | ||
fragments.record( | ||
{ | ||
rule: fragments.string('foo_library'), | ||
name: fragments.string('my_library'), | ||
srcs: fragments.array( | ||
[fragments.string('first'), fragments.string('second')], | ||
true | ||
), | ||
}, | ||
true | ||
), | ||
]); | ||
}); | ||
|
||
describe('.currentRecord', () => { | ||
it('returns the record fragment if it is current', () => { | ||
const ctx = new Ctx().startRecord(); | ||
expect(ctx.currentRecord).toEqual(fragments.record()); | ||
}); | ||
|
||
it('throws if there is no current', () => { | ||
const ctx = new Ctx(); | ||
expect(() => ctx.currentRecord).toThrow( | ||
new Error('Requested current, but no value.') | ||
); | ||
}); | ||
|
||
it('throws if the current is not a record fragment', () => { | ||
const ctx = new Ctx().startArray(); | ||
expect(() => ctx.currentRecord).toThrow( | ||
new Error('Requested current record, but does not exist.') | ||
); | ||
}); | ||
}); | ||
|
||
describe('.currentArray', () => { | ||
it('returns the array fragment if it is current', () => { | ||
const ctx = new Ctx().startArray(); | ||
expect(ctx.currentArray).toEqual(fragments.array()); | ||
}); | ||
|
||
it('throws if the current is not a record fragment', () => { | ||
const ctx = new Ctx().startRecord(); | ||
expect(() => ctx.currentArray).toThrow( | ||
new Error('Requested current array, but does not exist.') | ||
); | ||
}); | ||
}); | ||
|
||
it('throws if add an attribute without a parent', () => { | ||
const ctx = new Ctx().startAttribute('name'); | ||
expect(() => ctx.addString('chicken')).toThrow( | ||
new CtxProcessingError( | ||
fragments.attribute('name', fragments.string('chicken')) | ||
) | ||
); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
import type { | ||
AllFragments, | ||
ArrayFragment, | ||
ChildFragments, | ||
RecordFragment, | ||
} from './fragments'; | ||
import * as fragments from './fragments'; | ||
|
||
// Represents the fields that the context must have. | ||
export interface CtxCompatible { | ||
results: RecordFragment[]; | ||
stack: AllFragments[]; | ||
} | ||
|
||
export class CtxProcessingError extends Error { | ||
readonly current: AllFragments; | ||
readonly parent?: AllFragments; | ||
constructor(current: AllFragments, parent?: AllFragments) { | ||
const msg = `Invalid context state. current: ${current.type}, parent: ${ | ||
parent?.type ?? 'none' | ||
}`; | ||
super(msg); | ||
this.name = 'CtxProcessingError'; | ||
this.current = current; | ||
this.parent = parent; | ||
} | ||
} | ||
|
||
export class Ctx implements CtxCompatible { | ||
results: RecordFragment[]; | ||
stack: AllFragments[]; | ||
|
||
constructor(results: RecordFragment[] = [], stack: AllFragments[] = []) { | ||
this.results = results; | ||
this.stack = stack; | ||
} | ||
|
||
private get safeCurrent(): AllFragments | undefined { | ||
return this.stack.at(-1); | ||
} | ||
|
||
private get current(): AllFragments { | ||
const c = this.safeCurrent; | ||
if (c === undefined) { | ||
throw new Error('Requested current, but no value.'); | ||
} | ||
return c; | ||
} | ||
get currentRecord(): RecordFragment { | ||
const current = this.current; | ||
if (current.type === 'record') { | ||
return current; | ||
} | ||
throw new Error('Requested current record, but does not exist.'); | ||
} | ||
|
||
get currentArray(): ArrayFragment { | ||
const current = this.current; | ||
if (current.type === 'array') { | ||
return current; | ||
} | ||
throw new Error('Requested current array, but does not exist.'); | ||
} | ||
|
||
private popStack(): boolean { | ||
const current = this.stack.pop(); | ||
if (!current) { | ||
return false; | ||
} | ||
if (!current.isComplete) { | ||
this.stack.push(current); | ||
return false; | ||
} | ||
const parent = this.safeCurrent; | ||
|
||
if (parent) { | ||
if (parent.type === 'attribute' && fragments.isValue(current)) { | ||
parent.value = current; | ||
parent.isComplete = true; | ||
return true; | ||
} | ||
if (parent.type === 'array' && fragments.isPrimitive(current)) { | ||
parent.items.push(current); | ||
return true; | ||
} | ||
if ( | ||
parent.type === 'record' && | ||
current.type === 'attribute' && | ||
current.value !== undefined | ||
) { | ||
parent.children[current.name] = current.value; | ||
return true; | ||
} | ||
} else if (current.type === 'record') { | ||
this.results.push(current); | ||
return true; | ||
} | ||
|
||
throw new CtxProcessingError(current, parent); | ||
} | ||
|
||
private processStack(): Ctx { | ||
while (this.popStack()) { | ||
// Nothing to do | ||
} | ||
return this; | ||
} | ||
|
||
addString(value: string): Ctx { | ||
this.stack.push(fragments.string(value)); | ||
return this.processStack(); | ||
} | ||
|
||
addBoolean(value: string | boolean): Ctx { | ||
this.stack.push(fragments.boolean(value)); | ||
return this.processStack(); | ||
} | ||
|
||
startRecord(children: ChildFragments = {}): Ctx { | ||
const record = fragments.record(children); | ||
this.stack.push(record); | ||
return this; | ||
} | ||
|
||
endRecord(): Ctx { | ||
const record = this.currentRecord; | ||
record.isComplete = true; | ||
return this.processStack(); | ||
} | ||
|
||
startRule(name: string): Ctx { | ||
return this.startRecord({ rule: fragments.string(name) }); | ||
} | ||
|
||
endRule(): Ctx { | ||
return this.endRecord(); | ||
} | ||
|
||
startAttribute(name: string): Ctx { | ||
this.stack.push(fragments.attribute(name)); | ||
return this.processStack(); | ||
} | ||
|
||
startArray(): Ctx { | ||
this.stack.push(fragments.array()); | ||
return this.processStack(); | ||
} | ||
|
||
endArray(): Ctx { | ||
const array = this.currentArray; | ||
array.isComplete = true; | ||
return this.processStack(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import { codeBlock } from 'common-tags'; | ||
import { BazelDatasource } from '../../datasource/bazel'; | ||
import * as parser from './parser'; | ||
import { extractPackageFile } from '.'; | ||
|
||
describe('modules/manager/bazel-module/extract', () => { | ||
describe('extractPackageFile()', () => { | ||
it('returns null if fails to parse', () => { | ||
const result = extractPackageFile('blahhhhh:foo:@what\n', 'MODULE.bazel'); | ||
expect(result).toBeNull(); | ||
}); | ||
|
||
it('returns null if something throws an error', () => { | ||
jest.spyOn(parser, 'parse').mockImplementationOnce((input) => { | ||
throw new Error('Test error'); | ||
}); | ||
const result = extractPackageFile('content', 'MODULE.bazel'); | ||
expect(result).toBeNull(); | ||
}); | ||
|
||
it('returns null if file is empty', () => { | ||
const result = extractPackageFile('', 'MODULE.bazel'); | ||
expect(result).toBeNull(); | ||
}); | ||
|
||
it('returns null if file has not recognized declarations', () => { | ||
const input = codeBlock` | ||
ignore_me(name = "rules_foo", version = "1.2.3") | ||
`; | ||
const result = extractPackageFile(input, 'MODULE.bazel'); | ||
expect(result).toBeNull(); | ||
}); | ||
|
||
it('returns dependencies', () => { | ||
const input = codeBlock` | ||
bazel_dep(name = "rules_foo", version = "1.2.3") | ||
bazel_dep(name = "rules_bar", version = "1.0.0", dev_dependency = True) | ||
`; | ||
const result = extractPackageFile(input, 'MODULE.bazel'); | ||
expect(result).toEqual({ | ||
deps: [ | ||
{ | ||
datasource: BazelDatasource.id, | ||
depType: 'bazel_dep', | ||
depName: 'rules_foo', | ||
currentValue: '1.2.3', | ||
}, | ||
{ | ||
datasource: BazelDatasource.id, | ||
depType: 'bazel_dep', | ||
depName: 'rules_bar', | ||
currentValue: '1.0.0', | ||
}, | ||
], | ||
}); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.