Skip to content

Commit

Permalink
Improve draft rollout
Browse files Browse the repository at this point in the history
  • Loading branch information
daogrady committed Aug 24, 2023
1 parent fe85263 commit 462e5ba
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 49 deletions.
133 changes: 133 additions & 0 deletions lib/csn.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
const annotation = '@odata.draft.enabled'

class DraftUnroller {
/** @type {Set<string>} */
#positives = new Set()
/** @type {{[key: string]: boolean}} */
#draftable = {}
/** @type {{[key: string]: string}} */
#projections
/** @type {object[]} */
#entities
#csn
set csn(c) {
this.#csn = c
this.#entities = Object.values(c.definitions)
this.#projections = this.#entities.reduce((pjs, entity) => {
if (entity.projection) {
pjs[entity.name] = entity.projection.from.ref[0]
}
return pjs
}, {})
}
get csn() { return this.#csn }

/**
* @param entity {object | string} - entity to set draftable annotation for.
* @param value {boolean} - whether the entity is draftable.
*/
#setDraftable(entity, value) {
if (typeof entity === 'string') entity = this.#getDefinition(entity)
entity[annotation] = value
this.#draftable[entity.name] = value
if (value) {
this.#positives.add(entity.name)
} else {
this.#positives.delete(entity.name)
}
}

/**
* @param entity {object | string} - entity to look draftability up for.
* @returns {boolean}
*/
#getDraftable(entity) {
if (typeof entity === 'string') entity = this.#getDefinition(entity)
return this.#draftable[entity.name] ??= this.#propagateInheritance(entity)
}

/**
* @param name {string} - name of the entity.
*/
#getDefinition(name) { return this.csn.definitions[name] }

/**
* Propagate draft annotations through inheritance (includes).
* The latest annotation through the inheritance chain "wins".
* Annotations on the entity itself are always queued last, so they will always be decisive over ancestors.
*
* @param entity {object} - entity to pull draftability from its parents.
*/
#propagateInheritance(entity) {
const annotations = (entity.includes ?? []).map(parent => this.#getDraftable(parent))
annotations.push(entity[annotation])
this.#setDraftable(entity, annotations.filter(a => a !== undefined).at(-1) ?? false)
}

#propagateProjections() {
for (let [projection, target] of Object.entries(this.#projections)) {
do {
this.#setDraftable(target, this.#getDraftable(target) || this.#getDraftable(projection))
projection = target
target = this.#projections[target]
} while (target)
}
}

/**
* If an entity E is draftable and contains any composition of entities,
* then those entities also become draftable. Recursively.
*
* @param entity {object} - entity to propagate all compositions from.
*/
#propagateCompositions(entity) {
if (!this.#getDraftable(entity)) return

for (const comp of Object.values(entity.compositions ?? {})) {
const target = this.#getDefinition(comp.target)
const current = this.#getDraftable(target)
if (!current) {
this.#setDraftable(target, true)
this.#propagateCompositions(target)
}
}
}

unroll(csn) {
this.csn = csn

// inheritance
for (const entity of this.#entities) {
this.#propagateInheritance(entity)
}


// transitivity through compositions
// we have to do this in a second pass, as we only now know which entities are draft-enables themselves
for (const entity of this.#entities) {
this.#propagateCompositions(entity)
}

this.#propagateProjections()
}
}

/**
* We are unrolling the @odata.draft.enabled annotations into child entities manually.
* Note that when an entity extends two other entities of which one has drafts enabled and
* one has not, then the one that is later in the list of mixins "wins":
* @example
* ```ts
* @odata.draft.enabled true
* entity T {}
* @odata.draft.enabled false
* entity F {}
* entity A: T,F {} // draft not enabled
* entity B: F,T {} // draft enabled
* ```
*/
function unrollDraftability(csn) {
new DraftUnroller().unroll(csn)
}

module.exports = { unrollDraftability }
49 changes: 1 addition & 48 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -227,53 +227,7 @@ function fixCSN(csn) {
}
}

/**
* We are unrolling the @odata.draft.enabled annotations into child entities manually.
* Note that when an entity extends two other entities of which one has drafts enabled and
* one has not, then the one that is later in the list of mixins "wins":
* @example
* ```ts
* @odata.draft.enabled true
* entity T {}
* @odata.draft.enabled false
* entity F {}
* entity A: T,F {} // draft not enabled
* entity B: F,T {} // draft enabled
* ```
*/
function propagateAnnotations(csn) {
const annotation = '@odata.draft.enabled'
const draftable = {}
const positives = new Set()
const entities = Object.values(csn.definitions)

function get(entity) {
if(!(entity.name in draftable)) {
const annotations = (entity.includes ?? []).map(parent => get(csn.definitions[parent]))
annotations.push(entity[annotation])
const isDraftable = annotations.filter(a => a !== undefined).at(-1) ?? false
draftable[entity.name] = isDraftable
if (isDraftable) {
positives.add(entity.name)
}
}
return draftable[entity.name]
}

// inheritance
for (const entity of entities) {
entity[annotation] = get(entity)
}

// transitivity through compositions/ associations
// we have to do this in a second pass, as we only now know which entities are draft-enables themselves
for (const entity of entities) {
const refs = Object.values(entity.associations ?? {}).concat(Object.values(entity.compositions ?? {}))
for (const ref of refs) {
entity[annotation] ||= positives.has(ref.target)
}
}
}

module.exports = {
annotations,
Expand All @@ -284,6 +238,5 @@ module.exports = {
plural4,
parseCommandlineArgs,
deepMerge,
fixCSN,
propagateAnnotations
fixCSN
}
3 changes: 2 additions & 1 deletion lib/visitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const util = require('./util')

const { unrollDraftability } = require('./csn')
const { SourceFile, baseDefinitions, Buffer } = require('./file')
const { FlatInlineDeclarationResolver, StructuredInlineDeclarationResolver } = require('./components/inline')
const { Resolver } = require('./components/resolver')
Expand Down Expand Up @@ -59,7 +60,7 @@ class Visitor {
* @param {VisitorOptions} options
*/
constructor(csn, options = {}, logger = new Logger()) {
util.propagateAnnotations(csn)
unrollDraftability(csn)
this.options = { ...defaults, ...options }
this.logger = logger
this.csn = csn
Expand Down

0 comments on commit 462e5ba

Please sign in to comment.