Move codegen out of mizan-django: protocol/mizan-generate/
The codegen consumes a schema from any backend and emits typed client code for any frontend — it doesn't belong inside a backend adapter. That placement was historical sediment from when there was only a Django backend; it predates the AFI generalization. New top-level slot: `protocol/` for protocol-level tooling. Tree is now: backends/ server protocol adapters frontends/ client kernel + per-framework adapters cores/ shared language-level primitives protocol/ protocol-level tooling workers/ runtime workers / bridges Codegen moves to `protocol/mizan-generate/`. Same file layout under `generator/` (cli.mjs, lib/), preserved via git mv. Package metadata cleaned up: - name: "generate" (placeholder) → "mizan-generate" - description filled in - type: module (cli.mjs is .mjs ESM, was previously declared "commonjs") - bin entry added so `npx mizan-generate --config <config.mjs>` works once the package is published, instead of `node path/to/cli.mjs`. Path-reference fixups: - backends/mizan-django/README.md: `node path/to/...` → `npx mizan-generate` - backends/mizan-fastapi/README.md: same - ISSUES.md: file paths in three issue entries - CLAUDE.md: codegen description + Package Layout section refreshed (added protocol/, mizan-fastapi entry, mizan-python entry) - docs/AFI_ARCHITECTURE.md: Package Layout refreshed identically Verified codegen runs from new location: regenerated the FastAPI example harness's api/ output, identical to pre-move. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
117
protocol/mizan-generate/generator/lib/fetch.mjs
Normal file
117
protocol/mizan-generate/generator/lib/fetch.mjs
Normal file
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Schema Fetching — dispatches on the backend type configured in
|
||||
* `source.django` or `source.fastapi`.
|
||||
*
|
||||
* Both flavors spawn a Python subprocess that prints schema JSON to stdout:
|
||||
* Django: `python manage.py export_mizan_schema --indent 0`
|
||||
* FastAPI: `python -m mizan_fastapi.cli <module>`
|
||||
*/
|
||||
|
||||
import { spawn } from 'child_process'
|
||||
import path from 'path'
|
||||
|
||||
|
||||
function runSubprocess(cmd, args, opts) {
|
||||
const { cwd, env, label } = opts
|
||||
return new Promise((resolve, reject) => {
|
||||
const proc = spawn(cmd, args, {
|
||||
cwd,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
shell: process.platform === 'win32',
|
||||
env,
|
||||
})
|
||||
|
||||
let stdout = ''
|
||||
let stderr = ''
|
||||
|
||||
proc.stdout.on('data', (data) => { stdout += data.toString() })
|
||||
proc.stderr.on('data', (data) => { stderr += data.toString() })
|
||||
|
||||
proc.on('close', (code) => {
|
||||
if (code !== 0) {
|
||||
reject(new Error(`${label} command failed (exit ${code}):\n${stderr}`))
|
||||
return
|
||||
}
|
||||
|
||||
const jsonStart = stdout.indexOf('{')
|
||||
if (jsonStart === -1) {
|
||||
reject(new Error(`No JSON found in ${label} output:\n${stdout}\n${stderr}`))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
resolve(JSON.parse(stdout.slice(jsonStart)))
|
||||
} catch (err) {
|
||||
reject(new Error(`Failed to parse JSON from ${label}:\n${err.message}\n${stdout}`))
|
||||
}
|
||||
})
|
||||
|
||||
proc.on('error', (err) => {
|
||||
reject(new Error(`Failed to spawn ${label} command: ${err.message}`))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function runDjangoCommand(source, cwd, command) {
|
||||
const managePath = path.resolve(cwd, source.django.managePath)
|
||||
const manageDir = path.dirname(managePath)
|
||||
|
||||
let cmd, args
|
||||
if (source.django.command) {
|
||||
cmd = source.django.command[0]
|
||||
args = [...source.django.command.slice(1), 'manage.py', command, '--indent', '0']
|
||||
} else {
|
||||
const python = source.django.python || 'python'
|
||||
cmd = python
|
||||
args = [managePath, command, '--indent', '0']
|
||||
}
|
||||
|
||||
const env = source.django.env ? { ...process.env, ...source.django.env } : undefined
|
||||
return runSubprocess(cmd, args, { cwd: manageDir, env, label: 'Django' })
|
||||
}
|
||||
|
||||
|
||||
function runFastapiSchemaCommand(source, cwd) {
|
||||
const fastapiCwd = source.fastapi.cwd
|
||||
? path.resolve(cwd, source.fastapi.cwd)
|
||||
: cwd
|
||||
|
||||
let cmd, args
|
||||
if (source.fastapi.command) {
|
||||
cmd = source.fastapi.command[0]
|
||||
args = [...source.fastapi.command.slice(1), '-m', 'mizan_fastapi.cli', source.fastapi.module]
|
||||
} else {
|
||||
cmd = source.fastapi.python || 'python'
|
||||
args = ['-m', 'mizan_fastapi.cli', source.fastapi.module]
|
||||
}
|
||||
|
||||
const env = source.fastapi.env ? { ...process.env, ...source.fastapi.env } : undefined
|
||||
return runSubprocess(cmd, args, { cwd: fastapiCwd, env, label: 'FastAPI' })
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fetch channels schema. Channels are a Django-only feature; FastAPI
|
||||
* projects use native WebSockets and don't go through this path.
|
||||
*/
|
||||
export async function fetchChannelsSchema(source, cwd) {
|
||||
if (!source.django) {
|
||||
throw new Error('Channels schema export requires django source configuration')
|
||||
}
|
||||
return runDjangoCommand(source, cwd, 'export_channels_schema')
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fetch mizan schema. Dispatches on whichever backend source is configured.
|
||||
*/
|
||||
export async function fetchMizanSchema(source, cwd) {
|
||||
if (source.fastapi) {
|
||||
return runFastapiSchemaCommand(source, cwd)
|
||||
}
|
||||
if (source.django) {
|
||||
return runDjangoCommand(source, cwd, 'export_mizan_schema')
|
||||
}
|
||||
throw new Error('mizan schema export requires source.django or source.fastapi')
|
||||
}
|
||||
Reference in New Issue
Block a user