diff --git a/.github/workflows/gh-pages-report.yml b/.github/workflows/gh-pages-report.yml deleted file mode 100644 index fbf6d3d3e8..0000000000 --- a/.github/workflows/gh-pages-report.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Deploy GH pages report - -on: - workflow_dispatch: - push: - branches: - - "main" - paths: - - 'specification/**' - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Use Node.js 20 - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: Install - run: | - npm install --prefix compiler - - - name: Generate output - run: | - make generate - - - name: Generate graph - run: | - node compiler/reporter/index.js - - - name: Deploy 🚀 - uses: JamesIves/github-pages-deploy-action@v4.2.2 - with: - branch: gh-pages - folder: report diff --git a/compiler/reporter/create-import-graph.ts b/compiler/reporter/create-import-graph.ts deleted file mode 100644 index 0ca772ff86..0000000000 --- a/compiler/reporter/create-import-graph.ts +++ /dev/null @@ -1,238 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { readFile, writeFile } from 'fs/promises' -import { join } from 'path' -import * as model from '../src/model/metamodel' - -export interface ImportTypesGraph { - type: model.TypeName - imported_by: model.TypeName[] - imports: model.TypeName[] -} - -export interface ImportNamespaceGraph { - namespace: string - imported_by: string[] - imports: string[] -} - -async function createImportGraph (): Promise { - const model: model.Model = JSON.parse(await readFile(join(__dirname, '..', '..', 'output', 'schema', 'schema.json'), 'utf8')) - const importGraph: Map = new Map() - - for (const current of model.types) { - if (current.kind === 'request' || current.kind === 'response') continue - for (const type of model.types) { - if (current.name.name === type.name.name && current.name.namespace === type.name.namespace) continue - switch (type.kind) { - case 'type_alias': - if (contains(type.type, current)) { - const list = importGraph.get(current.name) ?? [] - importGraph.set(current.name, list.concat(type.name)) - } - break - - case 'interface': - for (const property of type.properties) { - if (contains(property.type, current)) { - const list = importGraph.get(current.name) ?? [] - importGraph.set(current.name, list.concat(type.name)) - } - } - if (type.inherits?.type.name === current.name.name && - type.inherits?.type.namespace === current.name.namespace) { - const list = importGraph.get(current.name) ?? [] - importGraph.set(current.name, list.concat(type.name)) - } - break - - case 'request': - for (const property of type.path) { - if (contains(property.type, current)) { - const list = importGraph.get(current.name) ?? [] - importGraph.set(current.name, list.concat(type.name)) - } - } - for (const property of type.query) { - if (contains(property.type, current)) { - const list = importGraph.get(current.name) ?? [] - importGraph.set(current.name, list.concat(type.name)) - } - } - if (type.body.kind === 'properties') { - for (const property of type.body.properties) { - if (contains(property.type, current)) { - const list = importGraph.get(current.name) ?? [] - importGraph.set(current.name, list.concat(type.name)) - } - } - } - if (type.inherits?.type.name === current.name.name && - type.inherits?.type.namespace === current.name.namespace) { - const list = importGraph.get(current.name) ?? [] - importGraph.set(current.name, list.concat(type.name)) - } - break - - case 'response': - if (type.body.kind === 'properties') { - for (const property of type.body.properties) { - if (contains(property.type, current)) { - const list = importGraph.get(current.name) ?? [] - importGraph.set(current.name, list.concat(type.name)) - } - } - } - if (type.inherits?.type.name === current.name.name && - type.inherits?.type.namespace === current.name.namespace) { - const list = importGraph.get(current.name) ?? [] - importGraph.set(current.name, list.concat(type.name)) - } - break - } - } - } - - const typeGraph: ImportTypesGraph[] = [] - for (const [type, types] of importGraph.entries()) { - typeGraph.push({ - type, - imported_by: types - .filter((type, index, arr) => { - return index === arr.findIndex(t => { - return type.name === t.name && type.namespace === t.namespace - }) - }), - imports: [] - }) - } - - for (const current of typeGraph) { - for (const entry of typeGraph) { - if (entry.imported_by.find(search)) { - current.imports.push(entry.type) - } - } - - function search (element: model.TypeName): boolean { - return element.name === current.type.name && element.namespace === current.type.namespace - } - } - - const namespaceMap: Map = new Map() - for (const [type, types] of importGraph.entries()) { - const list = namespaceMap.get(type.namespace) ?? [] - namespaceMap.set(type.namespace, list.concat(types.map(t => t.namespace))) - } - - const namespaceGraphExpanded: ImportNamespaceGraph[] = [] - for (const [namespace, namespaces] of namespaceMap.entries()) { - namespaceGraphExpanded.push({ - namespace, - imported_by: [...new Set(namespaces)].sort(), - imports: [] - }) - } - - for (const current of namespaceGraphExpanded) { - for (const entry of namespaceGraphExpanded) { - if (entry.imported_by.includes(current.namespace)) { - current.imports.push(entry.namespace) - } - } - } - - namespaceGraphExpanded.sort((a, b) => { - if (a.namespace < b.namespace) return -1 - if (a.namespace > b.namespace) return 1 - return 0 - }) - - const namespaceMapCompact: Map = new Map() - for (const entry of namespaceGraphExpanded) { - const store = namespaceMapCompact.get(compact(entry.namespace)) ?? { imported_by: [], imports: [] } - store.imports = [...new Set(store.imports.concat(entry.imports.map(compact)))] - store.imported_by = [...new Set(store.imported_by.concat(entry.imported_by.map(compact)))] - namespaceMapCompact.set(compact(entry.namespace), store) - } - - const namespaceGraphCompact: ImportNamespaceGraph[] = [] - for (const [namespace, data] of namespaceMapCompact.entries()) { - namespaceGraphCompact.push({ - namespace, - imported_by: data.imported_by, - imports: data.imports - }) - } - - await writeFile( - join(__dirname, '..', '..', 'output', 'schema', 'import-type-graph.json'), - JSON.stringify(typeGraph, null, 2), - 'utf8' - ) - - await writeFile( - join(__dirname, '..', '..', 'output', 'schema', 'import-namespace-graph-expanded.json'), - JSON.stringify(namespaceGraphExpanded, null, 2), - 'utf8' - ) - - await writeFile( - join(__dirname, '..', '..', 'output', 'schema', 'import-namespace-graph-compact.json'), - JSON.stringify(namespaceGraphCompact, null, 2), - 'utf8' - ) - - function contains (value: model.ValueOf, type: model.TypeDefinition): boolean { - switch (value.kind) { - case 'dictionary_of': - case 'array_of': - return contains(value.value, type) - - case 'union_of': - for (const item of value.items) { - if (contains(item, type)) return true - } - return false - - case 'instance_of': - return value.type.name === type.name.name && value.type.namespace === type.name.namespace - - case 'literal_value': - case 'user_defined_value': - return false - } - } - - function compact (namespace: string): string { - if (namespace.startsWith('_global')) { - return namespace.split('.')[1] - } - if (namespace.startsWith('_types')) { - return '_types' - } - return namespace.split('.')[0] - } -} - -createImportGraph().catch(err => { - console.error(err) - process.exit(1) -}) diff --git a/compiler/reporter/generate-import-graph.js b/compiler/reporter/generate-import-graph.js deleted file mode 100644 index 0be865d49a..0000000000 --- a/compiler/reporter/generate-import-graph.js +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -'use strict' - -// code adapted from https://observablehq.com/@d3/hierarchical-edge-bundling - -const minimist = require('minimist') -const { join } = require('path') -const { readFileSync, writeFileSync, mkdirSync } = require('fs') - -const options = minimist(process.argv.slice(2), { - boolean: ['expanded', 'compact'], - default: { - expanded: false, - compact: false - } -}) - -let file -if (options.expanded) { - file = join(__dirname, '..', '..', 'output', 'schema', 'import-namespace-graph-expanded.json') -} else if (options.compact) { - file = join(__dirname, '..', '..', 'output', 'schema', 'import-namespace-graph-compact.json') -} else { - console.error('You must specify --compact or --expanded') - process.exit(1) -} - -const rawData = JSON.parse(readFileSync(file, 'utf8')) -const data = { - name: 'root', - children: rawData.map(d => { - return { - name: d.namespace, - imports: d.imports, - imported_by: d.imported_by - } - }) -} - -const html = ` - - - - - - - -` - -function generate (window) { - const { d3, document } = window - const colorin = '#00f' - const colorout = '#f00' - const colornone = '#ccc' - const width = 500 - const radius = width / 2 - - const tree = d3.cluster().size([2 * Math.PI, radius - 100]) - const line = d3.lineRadial() - .curve(d3.curveBundle.beta(0.85)) - .radius(d => d.y) - .angle(d => d.x) - - const root = tree(bilink(d3.hierarchy(data).sort((a, b) => d3.ascending(a.height, b.height) || d3.ascending(a.data.name, b.data.name)))) - const body = d3.select(document).select('body') - - const svg = body - .append('svg') - .attr('viewBox', [-width / 2, -width / 2, width, width]) - - const node = svg.append('g') - .attr('font-family', 'sans-serif') - .attr('font-size', 5) - .selectAll('g') - .data(root.leaves()) - .join('g') - .attr('transform', d => `rotate(${d.x * 180 / Math.PI - 90}) translate(${d.y},0)`) - .append('text') - .attr('dy', '0.2em') - .attr('x', d => d.x < Math.PI ? 6 : -6) - .attr('text-anchor', d => d.x < Math.PI ? 'start' : 'end') - .attr('transform', d => d.x >= Math.PI ? 'rotate(180)' : null) - .text(d => d.data.name) - .each(function(d) { d.text = this; }) - .on('mouseover', overed) - .on('mouseout', outed) - .call(text => text.append('title').text(d => `${id(d).slice(5)} -${d.outgoing.length} outgoing -${d.incoming.length} incoming`)) - - const link = svg.append('g') - .attr('stroke', colornone) - .attr('fill', 'none') - .selectAll('path') - .data(root.leaves().flatMap(leaf => leaf.outgoing)) - .join('path') - .style('mix-blend-mode', 'multiply') - .attr('d', ([i, o]) => line(i.path(o))) - .each(function(d) { d.path = this; }) - - - function bilink (root) { - const map = new Map(root.leaves().map(d => [id(d), d])) - for (const d of root.leaves()) { - d.incoming = [] - d.outgoing = Array.isArray(d.data.imports) - ? d.data.imports.map(i => [d, map.get(`root.${i}`)]) - : [] - } - - for (const d of root.leaves()) { - for (const o of d.outgoing) { - o[1].incoming.push(o) - } - } - - return root - } - - function id (node) { - return `${node.parent ? id(node.parent) + '.' : ''}${node.data.name}` - } - - function overed (event, d) { - link.style('mix-blend-mode', null) - d3.select(this).attr('font-weight', 'bold') - d3.selectAll(d.incoming.map(d => d.path)).attr('stroke', colorin).raise() - d3.selectAll(d.incoming.map(([d]) => d.text)).attr('fill', colorin).attr('font-weight', 'bold') - d3.selectAll(d.outgoing.map(d => d.path)).attr('stroke', colorout).raise() - d3.selectAll(d.outgoing.map(([, d]) => d.text)).attr('fill', colorout).attr('font-weight', 'bold') - } - - function outed (event, d) { - link.style('mix-blend-mode', 'multiply') - d3.select(this).attr('font-weight', null) - d3.selectAll(d.incoming.map(d => d.path)).attr('stroke', null) - d3.selectAll(d.incoming.map(([d]) => d.text)).attr('fill', null).attr('font-weight', null) - d3.selectAll(d.outgoing.map(d => d.path)).attr('stroke', null) - d3.selectAll(d.outgoing.map(([, d]) => d.text)).attr('fill', null).attr('font-weight', null) - } -} - -mkdirSync(join(__dirname, '..', '..', 'report'), { recursive: true }) -writeFileSync( - join(__dirname, '..', '..', 'report', `namespace-dependencies-${options.compact ? 'compact' : 'expanded'}.html`), - html.trim(), - 'utf8' -) diff --git a/compiler/reporter/generate-type-report.ts b/compiler/reporter/generate-type-report.ts deleted file mode 100644 index 49138267f0..0000000000 --- a/compiler/reporter/generate-type-report.ts +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { readFile, writeFile } from 'fs/promises' -import { join } from 'path' -import { ImportTypesGraph, ImportNamespaceGraph } from './create-import-graph' -import * as model from '../src/model/metamodel' - -const tick = '`' -async function createTypeReport (): Promise { - const model: model.Model = JSON.parse(await readFile(join(__dirname, '..', '..', 'output', 'schema', 'schema.json'), 'utf8')) - const typesGraph: ImportTypesGraph[] = JSON.parse(await readFile(join(__dirname, '..', '..', 'output', 'schema', 'import-type-graph.json'), 'utf8')) - const namespaceGraph: ImportNamespaceGraph[] = JSON.parse(await readFile(join(__dirname, '..', '..', 'output', 'schema', 'import-namespace-graph-compact.json'), 'utf8')) - let markdown = '' - - markdown += '# Type Specification Report\n\n' - - markdown += '- [Compact namespace import graph](https://elastic.github.io/elasticsearch-specification/namespace-dependencies-compact.html)\n' - markdown += '- [Expanded namespace import graph](https://elastic.github.io/elasticsearch-specification/namespace-dependencies-expanded.html)\n' - markdown += '- [Most used namespaces](#most-used-namespaces)\n' - markdown += '- [Most greedy namespaces](#most-greedy-namespaces)\n' - markdown += '- [Top 50 used types](#top-50-used-types)\n' - markdown += '- [Top 50 greedy types](#top-50-greedy-types)\n' - - markdown += '\n\n## Most used namespaces\n' - markdown += '| Namespace | Occurencies |\n' - markdown += '| --- | --- |\n' - const mostUsedNamespaces = namespaceGraph.sort((a, b) => b.imported_by.length - a.imported_by.length) - for (const namespace of mostUsedNamespaces) { - markdown += `| ${tick}${namespace.namespace}${tick} | ${namespace.imported_by.length} |\n` - } - markdown += `\n[Back to top](#type-specification-report)\n` - - markdown += '\n\n## Most greedy namespaces\n' - markdown += '| Namespace | Occurencies |\n' - markdown += '| --- | --- |\n' - const mostGreedyNamespaces = namespaceGraph.sort((a, b) => b.imports.length - a.imports.length) - for (const namespace of mostGreedyNamespaces) { - markdown += `| ${tick}${namespace.namespace}${tick} | ${namespace.imports.length} |\n` - } - markdown += `\n[Back to top](#type-specification-report)\n` - - markdown += '\n\n## Top 50 used types\n' - markdown += '| Type | Occurencies | Location |\n' - markdown += '| --- | --- | --- |\n' - const mostUsedTypes = typesGraph.sort((a, b) => b.imported_by.length - a.imported_by.length) - for (let i = 0; i < 50; i++) { - const type = mostUsedTypes[i] - markdown += `| ${tick}${type.type.namespace}.${type.type.name}${tick} | ${type.imported_by.length} | ${findLocaton(type)} |\n` - } - markdown += `\n[Back to top](#type-specification-report)\n` - - markdown += '\n\n## Top 50 greedy types\n' - markdown += '| Type | Occurencies | Location |\n' - markdown += '| --- | --- | --- |\n' - const mostGreedyTypes = typesGraph.sort((a, b) => b.imports.length - a.imports.length) - for (let i = 0; i < 50; i++) { - const type = mostGreedyTypes[i] - markdown += `| ${tick}${type.type.namespace}.${type.type.name}${tick} | ${type.imports.length} | ${findLocaton(type)} |\n` - } - markdown += `\n[Back to top](#type-specification-report)\n` - - await writeFile( - join(__dirname, '..', '..', 'report', 'report.md'), - markdown, - 'utf8' - ) - - function findLocaton (type: ImportTypesGraph): string { - for (const current of model.types) { - if (current.name.name === type.type.name && current.name.namespace === type.type.namespace) { - return `[${tick}${current.specLocation.path}${tick}](https://github.com/elastic/elasticsearch-specification/blob/main/specification/${current.specLocation.path}#L${current.specLocation.startLine}-L${current.specLocation.endLine})` - } - } - throw new Error(`Can't find type ${JSON.stringify(type.type)}`) - } -} - -createTypeReport().catch(err => { - console.error(err) - process.exit(1) -}) diff --git a/compiler/reporter/index.js b/compiler/reporter/index.js deleted file mode 100644 index 78b5f8de03..0000000000 --- a/compiler/reporter/index.js +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/* global $ argv, path, cd, nothrow */ - -'use strict' - -require('zx/globals') -const { writeFile } = require('fs/promises') - -const compilerPath = path.join(__dirname, '..') -const tsNode = path.join(compilerPath, 'node_modules', '.bin', 'ts-node') - -const jekyllConfig = ` -markdown: GFM -` - -async function run () { - await $`${tsNode} ${path.join(compilerPath, 'reporter', 'create-import-graph.ts')}` - await $`node ${path.join(compilerPath, 'reporter', 'generate-import-graph.js')} --compact` - await $`node ${path.join(compilerPath, 'reporter', 'generate-import-graph.js')} --expanded` - await $`${tsNode} ${path.join(compilerPath, 'reporter', 'generate-type-report.ts')}` - await writeFile( - path.join(__dirname, '..', '..', 'report', '_config.yml'), - jekyllConfig.trim(), - 'utf8' - ) -} - -run().catch(err => { - console.error(err) - process.exit(1) -})