feat: publish Dever core with project scaffolding and versioned AI skills

This commit is contained in:
Antigravity committed 2026-10-04 13:37:30 +00:00
1 parent 70fa73cdc2
commit ac458ffa7f
960 files changed
+221319 -1163

No files matched your search

+241
View File
@@ -0,0 +1,241 @@
#[path = "support/temp.rs"]
mod temp;
use std::fs;
use dever_core::source::SourceMap;
use dever_runtime::api::{self, Inputs};
use dever_runtime::bytes::Bytes;
use dever_runtime::config::Settings;
use dever_runtime::http::{Header, Request};
use temp::TemporaryDirectory;
fn sources(main: &str, api_source: &str) -> SourceMap {
let mut sources = SourceMap::default();
sources.add("main.dever", main);
sources.add("user/api/hello.dever", api_source);
sources.add(
"user/service/greeting.dever",
"public type Greeting { message: Text }\n\
public greet(name: Text) (greeting: Greeting) {\n\
greeting = Greeting { message = \"hello, \" + text.trim(name) }\n\
}\n",
);
sources
}
fn diagnostics(sources: &SourceMap) -> String {
dever_core::check(sources)
.expect_err("source must be rejected")
.iter()
.map(|error| error.render(sources))
.collect()
}
#[test]
fn explicit_serve_compiles_recursive_module_routes_and_json_adapter() {
let mut sources = sources(
"main() () { dever.api.serve() }",
"get_greeting(name: Text) (response: user.service.greeting.Greeting) {\n\
response = user.service.greeting.greet(name)\n\
}\n\
post_greeting(name: Text) (response: user.service.greeting.Greeting) {\n\
response = user.service.greeting.greet(name)\n\
}",
);
sources.add(
"user/api/admin/health.dever",
"get_status() (response: Text) { response = \"ready\" }",
);
let program = dever_core::check(&sources).unwrap_or_else(|errors| {
panic!(
"{}",
errors
.iter()
.map(|error| error.render(&sources))
.collect::<String>()
)
});
let generated = dever_core::native::emit(&program, &sources, "main.main").unwrap();
assert!(generated.contains("(\"GET\", \"/user/hello/greeting\")"));
assert!(generated.contains("(\"POST\", \"/user/hello/greeting\")"));
assert!(generated.contains("(\"GET\", \"/user/admin/health/status\")"));
assert!(generated.contains("dever_runtime::api::serve(api_route).await"));
assert!(generated.contains("inputs.finish()"));
assert!(generated.contains("dever_runtime::api::success(value)"));
}
#[test]
fn generated_api_application_compiles_without_starting_a_listener() {
let sources = sources(
"main() () { dever.api.serve() }",
"get_greeting(name: Text) (response: user.service.greeting.Greeting) {\n\
response = user.service.greeting.greet(name)\n\
}\n\
post_greeting(name: Text) (response: user.service.greeting.Greeting) {\n\
response = user.service.greeting.greet(name)\n\
}\n\
delete_greeting(name: Text) (response: Text) { response = name }",
);
let program = dever_core::check(&sources).unwrap();
let rustc = std::env::var_os("RUSTC").unwrap_or_else(|| "rustc".into());
let native = dever_core::native::compile_application(
&program,
&sources,
"main.main",
&rustc,
dever_runtime::config::RuntimeProfile::default(),
)
.unwrap();
assert!(native.executable().is_file());
}
#[test]
fn without_serve_api_sources_are_checked_but_not_emitted() {
let checked_sources = sources(
"main() (answer: Int) { answer = 42 }",
"get_greeting(name: Text) (response: user.service.greeting.Greeting) {\n\
response = user.service.greeting.greet(name)\n\
}",
);
let program = dever_core::check(&checked_sources).unwrap();
let generated = dever_core::native::emit(&program, &checked_sources, "main.main").unwrap();
assert!(!generated.contains("fn api_route("));
assert!(!generated.contains("dever_runtime::api::serve"));
let invalid = sources(
"main() () {}",
"fetch_greeting() (response: Text) { response = \"x\" }",
);
assert!(diagnostics(&invalid).contains("API handler names must start with"));
}
#[test]
fn api_only_accepts_private_http_handlers_with_response_output() {
for (source, expected) in [
(
"public get_ok() (response: Text) { response = \"x\" }",
"private ordinary",
),
(
"post_create() (result: Text) { result = \"x\" }",
"named response",
),
(
"delete_thing(file: dever.system.File) (response: Text) { response = \"x\" }",
"API inputs must",
),
(
"get_limit(value: Int >= 1) (response: Int) { response = value }",
"API inputs must be plain",
),
(
"type Temporary { value: Text }",
"only HTTP handler functions",
),
] {
let errors = diagnostics(&sources("main() () {}", source));
assert!(errors.contains(expected), "{source}: {errors}");
}
}
#[test]
fn query_and_json_body_bind_the_same_typed_parameters() {
let request = Request {
method: "GET".into(),
target: "/user/hello/greeting?name=%E4%B8%AD%E6%96%87&count=12&active=true".into(),
headers: vec![],
body: Bytes::from_text(""),
};
let mut inputs = Inputs::from_request(&request).unwrap();
assert_eq!(inputs.text("name").unwrap(), "中文");
assert_eq!(inputs.int("count").unwrap(), 12);
assert!(inputs.boolean("active").unwrap());
inputs.finish().unwrap();
let request = Request {
method: "POST".into(),
target: "/user/hello/greeting".into(),
headers: vec![Header {
name: "content-type".into(),
value: Bytes::from_text("application/json"),
}],
body: Bytes::from_text("{\"name\":\"Ada\",\"count\":2,\"active\":false}"),
};
let mut inputs = Inputs::from_request(&request).unwrap();
assert_eq!(inputs.text("name").unwrap(), "Ada");
assert_eq!(inputs.int("count").unwrap(), 2);
assert!(!inputs.boolean("active").unwrap());
inputs.finish().unwrap();
}
#[test]
fn malformed_or_extra_inputs_get_structured_errors() {
for target in [
"/user/hello?name=x&name=y",
"/user/hello?name=%GG",
"/user/hello?name=x&extra=1",
] {
let request = Request {
method: "GET".into(),
target: target.into(),
headers: vec![],
body: Bytes::from_text(""),
};
let parsed = Inputs::from_request(&request);
if let Ok(mut inputs) = parsed {
inputs.text("name").unwrap();
assert!(inputs.finish().is_err());
}
}
let response = api::invalid_input(api::InputError("bad value".into()));
assert_eq!(response.status, 400);
let body: api::Value = serde_json::from_slice(response.body.values()).unwrap();
assert_eq!(body["code"], 400);
assert_eq!(body["message"], "bad value");
assert!(body["data"].is_null());
for (status, message) in [
(400, "invalid request"),
(401, "unauthorized"),
(403, "forbidden"),
(404, "not found"),
(409, "conflict"),
(429, "too many requests"),
] {
let response = api::standard_error(status);
assert_eq!(response.status, status);
let body: api::Value = serde_json::from_slice(response.body.values()).unwrap();
assert_eq!(body["code"], status);
assert_eq!(body["message"], message);
assert!(body["data"].is_null());
}
}
#[test]
fn listener_settings_are_required_only_for_api_and_come_from_project_config() {
let directory = TemporaryDirectory::new();
fs::create_dir(directory.path().join("config")).unwrap();
let setting = directory.path().join("config/setting.json");
fs::write(&setting, "{}\n").unwrap();
let settings = Settings::load_project(directory.path()).unwrap();
assert!(
settings
.http()
.unwrap_err()
.contains("http.host and http.port")
);
fs::write(&setting, r#"{"http":{"host":"127.0.0.1","port":8181}}"#).unwrap();
let settings = Settings::load_project(directory.path()).unwrap();
let http = settings.http().unwrap();
assert_eq!(http.host, "127.0.0.1");
assert_eq!(http.port, 8181);
fs::write(&setting, r#"{"http":{"host":"example.com","port":8181}}"#).unwrap();
assert!(
Settings::load_project(directory.path())
.unwrap()
.http()
.is_err()
);
}