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

add support for serializing parts of an element separately #153

Merged
merged 1 commit into from
Sep 10, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
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
133 changes: 77 additions & 56 deletions packages/salto/src/parser/salto.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import _ from 'lodash'
import {
Type, ElemID, ObjectType, PrimitiveType, PrimitiveTypes, Field, Values,
isObjectType, isPrimitiveType, Element, isInstanceElement, InstanceElement,
Type, ElemID, ObjectType, PrimitiveType, PrimitiveTypes, Field, Values, isObjectType,
isPrimitiveType, Element, isInstanceElement, InstanceElement, isField, isElement,
} from 'adapter-api'
import { collections } from '@salto/lowerdash'
import HCLParser, {
Expand Down Expand Up @@ -212,7 +212,7 @@ export default class Parser {
return {
type: Keywords.LIST_DEFINITION,
labels: [field.type.elemID.getFullName(), field.name],
attrs: field.getAnnotationsValues() || {},
attrs: field.getAnnotationsValues(),
blocks: [],
}
}
Expand All @@ -221,78 +221,99 @@ export default class Parser {
return {
type: field.type.elemID.getFullName(),
labels: [field.name],
attrs: field.getAnnotationsValues() || {},
attrs: field.getAnnotationsValues(),
blocks: [],
}
}

private static getAnnotationsBlock(element: PrimitiveType): HCLBlock {
return {
private static getAnnotationsBlock(element: Type): HCLBlock[] {
return _.isEmpty(element.annotations) ? [] : [{
type: Keywords.ANNOTATIONS_DEFINITION,
labels: [],
attrs: {},
blocks: Object.keys(element.annotations)
.map(key => this.getFieldBlock(new Field(element.elemID, key, element.annotations[key]))),
blocks: Object.entries(element.annotations).map(([key, type]) => ({
type: type.elemID.getFullName(),
labels: [key],
attrs: {},
blocks: [],
})),
}]
}

private static getElementBlock(elem: Element): HCLBlock {
if (isObjectType(elem)) {
return {
type: Keywords.TYPE_DEFINITION,
labels: [elem.elemID.getFullName()],
attrs: elem.getAnnotationsValues(),
blocks: this.getAnnotationsBlock(elem).concat(
Object.values(elem.fields).map(f => this.getBlock(f))
),
}
}
if (isPrimitiveType(elem)) {
return {
type: Keywords.TYPE_DEFINITION,
labels: [
elem.elemID.getFullName(),
Keywords.TYPE_INHERITENCE_SEPARATOR,
getPrimitiveTypeName(elem.primitive),
],
attrs: elem.getAnnotationsValues(),
blocks: this.getAnnotationsBlock(elem),
}
}
if (isInstanceElement(elem)) {
return {
type: elem.type.elemID.getFullName(),
labels: elem.elemID.isConfig() ? [] : [elem.elemID.name],
attrs: elem.value,
blocks: [],
}
}
// Without this exception the linter won't allow us to end the function
// without a return value
throw new Error('Unsupported element type')
}

private static getBlock(value: Element | Values): HCLBlock {
if (isField(value)) {
return value.isList ? this.getListFieldBlock(value) : this.getFieldBlock(value)
}
if (isElement(value)) {
return this.getElementBlock(value)
}
// If we reach this point we are serializing values
return {
type: '',
labels: [],
attrs: value as Values,
blocks: [],
}
}

/**
* Serialize elements to blueprint
*
* @param elements The elements to serialize
* @param elementsOrValues The element(s) or attributes to serialize
* @returns A buffer with the elements serialized as a blueprint
*/
public static async dump(elements: Element[]): Promise<string> {
const blocks = elements.map(elem => {
if (isObjectType(elem)) {
// Clone the annotation values because we may delete some keys from there
const annotationsValues = _.cloneDeep(elem.getAnnotationsValues())
return {
type: Keywords.TYPE_DEFINITION,
labels: [elem.elemID.getFullName()],
attrs: annotationsValues,
blocks: Object.values(elem.fields).map(field => ((field.isList)
? this.getListFieldBlock(field)
: this.getFieldBlock(field))),
}
}
if (isPrimitiveType(elem)) {
return {
type: Keywords.TYPE_DEFINITION,
labels: [
elem.elemID.getFullName(),
Keywords.TYPE_INHERITENCE_SEPARATOR,
getPrimitiveTypeName(elem.primitive),
],
attrs: elem.getAnnotationsValues(),
blocks: Object.keys(elem.annotations).length > 0 ? [this.getAnnotationsBlock(elem)] : [],
}
}
if (isInstanceElement(elem)) {
const labels = elem.elemID.name === ElemID.CONFIG_INSTANCE_NAME
? []
: [elem.elemID.name]

return {
type: elem.type.elemID.getFullName(),
labels,
attrs: elem.value,
blocks: [],
}
}

// Without this exception the linter won't allow us to end the function
// without a return value
throw new Error('unsupported type')
})

const body: HCLBlock = {
static async dump(elementsOrValues: Element | Element[] | Values): Promise<string> {
const wrapBlocks = (blocks: HCLBlock[]): HCLBlock => ({
type: '',
labels: [],
attrs: {},
blocks: blocks.map(markBlockQuotes),
}
blocks,
})

// If we got a single element, put it in an array because we need to wrap it with an empty block
const elemListOrValues = isElement(elementsOrValues) ? [elementsOrValues] : elementsOrValues

const body = _.isArray(elemListOrValues)
Copy link
Contributor

Choose a reason for hiding this comment

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

is it possible that elemListOrValues is not an array?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, if we get Values (they are an object, not a list)

Copy link
Contributor

Choose a reason for hiding this comment

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

👍

? wrapBlocks(elemListOrValues.map(e => this.getBlock(e)))
: this.getBlock(elemListOrValues)

body.blocks = body.blocks.map(markBlockQuotes)
return removeQuotes(await HCLParser.dump(body))
}
}
120 changes: 72 additions & 48 deletions packages/salto/test/parser/salto.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,64 +380,88 @@ describe('Salto Dump', () => {
}
)

let body: string
describe('dump elements', () => {
let body: string

beforeAll(async () => {
body = await Parser.dump([strType, numType, boolType, fieldType, model, instance, config])
})
beforeAll(async () => {
body = await Parser.dump([strType, numType, boolType, fieldType, model, instance, config])
})

it('dumps primitive types', () => {
expect(body).toMatch(/type salesforce_string is string {/)
expect(body).toMatch(/type salesforce_number is number {/)
expect(body).toMatch(/type salesforce_bool is boolean {/)
})
it('dumps primitive types', () => {
expect(body).toMatch(/type salesforce_string is string {/)
expect(body).toMatch(/type salesforce_number is number {/)
expect(body).toMatch(/type salesforce_bool is boolean {/)
})

it('dumps primitive field type annotations', () => {
expect(body).toMatch(/type salesforce_field is number {.*?annotations {.*?number alice {/s)
expect(body).toMatch(/type salesforce_field is number {.*?annotations {.*?number bob {/s)
expect(body).toMatch(/type salesforce_field is number {.*?annotations {.*?boolean tom {/s)
expect(body).toMatch(/type salesforce_field is number {.*?annotations {.*?string jerry {/s)
})
it('dumps primitive field type annotations', () => {
expect(body).toMatch(/type salesforce_field is number {.*?annotations {.*?number alice {/s)
expect(body).toMatch(/type salesforce_field is number {.*?annotations {.*?number bob {/s)
expect(body).toMatch(/type salesforce_field is number {.*?annotations {.*?boolean tom {/s)
expect(body).toMatch(/type salesforce_field is number {.*?annotations {.*?string jerry {/s)
})

it('dumps instance elements', () => {
expect(body).toMatch(/salesforce_test me {/)
})
it('dumps instance elements', () => {
expect(body).toMatch(/salesforce_test me {/)
})

it('dumps config elements', () => {
expect(body).toMatch(/salesforce_test {/)
})

it('dumps config elements', () => {
expect(body).toMatch(/salesforce_test {/)
describe('dumped model', () => {
it('has correct block type and label', () => {
expect(body).toMatch(/type salesforce_test {/)
})
it('has complex attributes', () => {
expect(body).toMatch(
/lead_convert_settings = {\s*account = \[\s*{\s*input\s*=\s*"bla",\s*output\s*=\s*"foo"\s*}\s*\]\s*}/m,
)
})
it('has fields', () => {
expect(body).toMatch(
/salesforce_string name {\s+label = "Name"\s+}/m,
)
expect(body).toMatch(
/salesforce_number num {/m,
)
expect(body).toMatch(
/list salesforce_string list {/m
)
})
it('can be parsed back', async () => {
const { elements, errors } = await Parser.parse(Buffer.from(body), 'none')
expect(errors.length).toEqual(0)
expect(elements.length).toEqual(7)
expect(elements[0]).toEqual(strType)
expect(elements[1]).toEqual(numType)
expect(elements[2]).toEqual(boolType)
TestHelpers.expectTypesToMatch(elements[3] as Type, fieldType)
TestHelpers.expectTypesToMatch(elements[4] as Type, model)
TestHelpers.expectInstancesToMatch(elements[5] as InstanceElement, instance)
TestHelpers.expectInstancesToMatch(elements[6] as InstanceElement, config)
})
})
})
describe('dump field', () => {
let body: string

describe('dumped model', () => {
it('has correct block type and label', () => {
expect(body).toMatch(/type salesforce_test {/)
beforeAll(async () => {
body = await Parser.dump(model.fields.name)
})
it('has complex attributes', () => {
expect(body).toMatch(
/lead_convert_settings = {\s*account = \[\s*{\s*input\s*=\s*"bla",\s*output\s*=\s*"foo"\s*}\s*\]\s*}/m,
)

it('should contain only field', () => {
expect(body).toMatch(/^salesforce_string name {\s+label = "Name"\s+}$/m)
})
it('has fields', () => {
expect(body).toMatch(
/salesforce_string name {\s+label = "Name"\s+}/m,
)
expect(body).toMatch(
/salesforce_number num {/m,
)
expect(body).toMatch(
/list salesforce_string list {/m
)
})
describe('dump attribute', () => {
let body: string

beforeAll(async () => {
body = await Parser.dump({ attr: 'value' })
})
it('can be parsed back', async () => {
const { elements, errors } = await Parser.parse(Buffer.from(body), 'none')
expect(errors.length).toEqual(0)
expect(elements.length).toEqual(7)
expect(elements[0]).toEqual(strType)
expect(elements[1]).toEqual(numType)
expect(elements[2]).toEqual(boolType)
TestHelpers.expectTypesToMatch(elements[3] as Type, fieldType)
TestHelpers.expectTypesToMatch(elements[4] as Type, model)
TestHelpers.expectInstancesToMatch(elements[5] as InstanceElement, instance)
TestHelpers.expectInstancesToMatch(elements[6] as InstanceElement, config)

it('should contain only attribute', () => {
expect(body).toMatch(/^attr\s+=\s+"value"$/m)
})
})
})