misardev 1.1.0

Async Rust client for the Misar.Dev API: create projects, write files, build and deploy, start from templates, run OpenAI-compatible chat completions, search code and read usage. Retries, typed plan-limit errors, SSE streaming.
Documentation
// GENERATED by scripts/generate-sdk-resources.mjs from sdks/manifest.json.
// Do not edit by hand — change the route, run `pnpm sdk:sync`, and regenerate.

//! Every operation in sdks/manifest.json, called for real against a mock
//! server: each must build the exact verb and URL the server routes.

use futures::future::BoxFuture;
use futures::StreamExt;
use serde_json::{json, Value};
use wiremock::matchers::any;
use wiremock::{Mock, MockServer, ResponseTemplate};

use crate::{EventStream, MisarDev, MisarDevError};

type Call = for<'a> fn(&'a MisarDev) -> BoxFuture<'a, Result<Value, MisarDevError>>;
type StreamCall = for<'a> fn(&'a MisarDev) -> BoxFuture<'a, Result<EventStream, MisarDevError>>;

/// (name, verb, expected path + query, has body, call)
pub(crate) const OPERATIONS: &[(&str, &str, &str, bool, Call)] = &[
    ("account.createKey", "POST", "/account/api-keys", true, |d| Box::pin(d.account().create_key(Some(json!({"probe": true}))))),
    ("account.deleteKey", "DELETE", "/account/api-keys/keyId-1", false, |d| Box::pin(d.account().delete_key("keyId-1"))),
    ("account.export", "GET", "/account/export", false, |d| Box::pin(d.account().export())),
    ("account.listKeys", "GET", "/account/api-keys", false, |d| Box::pin(d.account().list_keys())),
    ("account.updateKey", "PATCH", "/account/api-keys/keyId-1", true, |d| Box::pin(d.account().update_key("keyId-1", Some(json!({"probe": true}))))),
    ("apiKeys.create", "POST", "/api-keys", true, |d| Box::pin(d.api_keys().create(Some(json!({"probe": true}))))),
    ("apiKeys.delete", "DELETE", "/api-keys", true, |d| Box::pin(d.api_keys().delete(Some(json!({"probe": true}))))),
    ("apiKeys.list", "GET", "/api-keys", false, |d| Box::pin(d.api_keys().list(None))),
    ("audit.list", "GET", "/audit", false, |d| Box::pin(d.audit().list())),
    ("billing.checkout", "POST", "/billing/checkout", true, |d| Box::pin(d.billing().checkout(Some(json!({"probe": true}))))),
    ("billing.comparison", "GET", "/billing/comparison", false, |d| Box::pin(d.billing().comparison())),
    ("billing.plans", "GET", "/billing/plans", false, |d| Box::pin(d.billing().plans())),
    ("billing.subscription", "GET", "/billing/subscription", false, |d| Box::pin(d.billing().subscription())),
    ("billing.switchPlan", "POST", "/billing/switch-plan", true, |d| Box::pin(d.billing().switch_plan(Some(json!({"probe": true}))))),
    ("builds.list", "GET", "/builds", false, |d| Box::pin(d.builds().list(None))),
    ("chat.completions", "POST", "/v1/chat/completions", true, |d| Box::pin(d.chat().completions(Some(json!({"probe": true}))))),
    ("checkpoints.delete", "DELETE", "/checkpoints/id-1", false, |d| Box::pin(d.checkpoints().delete("id-1"))),
    ("checkpoints.restore", "POST", "/checkpoints/id-1/restore", false, |d| Box::pin(d.checkpoints().restore("id-1"))),
    ("checkpoints.update", "PATCH", "/checkpoints/id-1", true, |d| Box::pin(d.checkpoints().update("id-1", Some(json!({"probe": true}))))),
    ("closeFriends.add", "POST", "/close-friends", true, |d| Box::pin(d.close_friends().add(Some(json!({"probe": true}))))),
    ("closeFriends.list", "GET", "/close-friends", false, |d| Box::pin(d.close_friends().list(None))),
    ("closeFriends.remove", "DELETE", "/close-friends", true, |d| Box::pin(d.close_friends().remove(Some(json!({"probe": true}))))),
    ("collections.addPost", "POST", "/collections/id-1/items", true, |d| Box::pin(d.collections().add_post("id-1", Some(json!({"probe": true})), None))),
    ("collections.create", "POST", "/collections", true, |d| Box::pin(d.collections().create(Some(json!({"probe": true}))))),
    ("collections.delete", "DELETE", "/collections/id-1", false, |d| Box::pin(d.collections().delete("id-1"))),
    ("collections.list", "GET", "/collections", false, |d| Box::pin(d.collections().list())),
    ("collections.removePost", "DELETE", "/collections/id-1/items", true, |d| Box::pin(d.collections().remove_post("id-1", Some(json!({"probe": true})), None))),
    ("collections.update", "PATCH", "/collections/id-1", true, |d| Box::pin(d.collections().update("id-1", Some(json!({"probe": true}))))),
    ("comments.delete", "DELETE", "/comments/id-1", false, |d| Box::pin(d.comments().delete("id-1"))),
    ("comments.like", "POST", "/comments/id-1/like", false, |d| Box::pin(d.comments().like("id-1"))),
    ("comments.unlike", "DELETE", "/comments/id-1/like", false, |d| Box::pin(d.comments().unlike("id-1"))),
    ("comments.update", "PATCH", "/comments/id-1", true, |d| Box::pin(d.comments().update("id-1", Some(json!({"probe": true}))))),
    ("connectors.available", "GET", "/connectors/available", false, |d| Box::pin(d.connectors().available())),
    ("connectors.createConnection", "POST", "/connectors/connections", true, |d| Box::pin(d.connectors().create_connection(Some(json!({"probe": true}))))),
    ("connectors.deleteConnection", "DELETE", "/connectors/connections/id-1", false, |d| Box::pin(d.connectors().delete_connection("id-1"))),
    ("connectors.list", "GET", "/connectors", false, |d| Box::pin(d.connectors().list())),
    ("connectors.listConnections", "GET", "/connectors/connections", false, |d| Box::pin(d.connectors().list_connections(None))),
    ("connectors.testConnection", "POST", "/connectors/connections/id-1/test", false, |d| Box::pin(d.connectors().test_connection("id-1"))),
    ("connectors.toggle", "POST", "/connectors/slug-1/toggle", true, |d| Box::pin(d.connectors().toggle("slug-1", Some(json!({"probe": true}))))),
    ("connectors.updateConnection", "PATCH", "/connectors/connections/id-1", true, |d| Box::pin(d.connectors().update_connection("id-1", Some(json!({"probe": true}))))),
    ("dm.acceptThread", "POST", "/dm/threads/id-1/accept", false, |d| Box::pin(d.dm().accept_thread("id-1"))),
    ("dm.createThread", "POST", "/dm/threads", true, |d| Box::pin(d.dm().create_thread(Some(json!({"probe": true}))))),
    ("dm.createThreadsMute", "POST", "/dm/threads/id-1/mute", false, |d| Box::pin(d.dm().create_threads_mute("id-1"))),
    ("dm.declineThread", "POST", "/dm/threads/id-1/decline", false, |d| Box::pin(d.dm().decline_thread("id-1"))),
    ("dm.deleteThreadsMute", "DELETE", "/dm/threads/id-1/mute", false, |d| Box::pin(d.dm().delete_threads_mute("id-1"))),
    ("dm.listMessages", "GET", "/dm/threads/id-1/messages", false, |d| Box::pin(d.dm().list_messages("id-1", None))),
    ("dm.listThreads", "GET", "/dm/threads", false, |d| Box::pin(d.dm().list_threads(None))),
    ("dm.markThreadRead", "POST", "/dm/threads/id-1/read", false, |d| Box::pin(d.dm().mark_thread_read("id-1"))),
    ("dm.sendMessage", "POST", "/dm/threads/id-1/messages", true, |d| Box::pin(d.dm().send_message("id-1", Some(json!({"probe": true}))))),
    ("dm.unreadCounts", "GET", "/dm/unread", false, |d| Box::pin(d.dm().unread_counts())),
    ("domains.checkout", "POST", "/domains/checkout", true, |d| Box::pin(d.domains().checkout(Some(json!({"probe": true}))))),
    ("domains.createDnsRecord", "POST", "/domains/id-1/dns", true, |d| Box::pin(d.domains().create_dns_record("id-1", Some(json!({"probe": true}))))),
    ("domains.deleteDnsRecord", "DELETE", "/domains/id-1/dns?recordId=recordId-v", false, |d| Box::pin(d.domains().delete_dns_record("id-1", "recordId-v"))),
    ("domains.list", "GET", "/domains", false, |d| Box::pin(d.domains().list())),
    ("domains.listDnsRecords", "GET", "/domains/id-1/dns", false, |d| Box::pin(d.domains().list_dns_records("id-1"))),
    ("domains.lookup", "GET", "/domains/lookup?domain=domain-v", false, |d| Box::pin(d.domains().lookup("domain-v"))),
    ("domains.search", "POST", "/domains/search", true, |d| Box::pin(d.domains().search(Some(json!({"probe": true}))))),
    ("domains.update", "PATCH", "/domains/id-1", true, |d| Box::pin(d.domains().update("id-1", Some(json!({"probe": true}))))),
    ("feed.following", "GET", "/feed", false, |d| Box::pin(d.feed().following(None))),
    ("feed.forYou", "GET", "/feed/for-you", false, |d| Box::pin(d.feed().for_you(None))),
    ("gallery.like", "POST", "/gallery/projectId-1/like", false, |d| Box::pin(d.gallery().like("projectId-1"))),
    ("gallery.list", "GET", "/gallery", false, |d| Box::pin(d.gallery().list(None))),
    ("gallery.unlike", "DELETE", "/gallery/projectId-1/like", false, |d| Box::pin(d.gallery().unlike("projectId-1"))),
    ("github.disconnect", "DELETE", "/github/connection", false, |d| Box::pin(d.github().disconnect())),
    ("github.getConnection", "GET", "/github/connection", false, |d| Box::pin(d.github().get_connection())),
    ("highlights.addStory", "POST", "/highlights/id-1/items", true, |d| Box::pin(d.highlights().add_story("id-1", Some(json!({"probe": true}))))),
    ("highlights.create", "POST", "/highlights", true, |d| Box::pin(d.highlights().create(Some(json!({"probe": true}))))),
    ("highlights.delete", "DELETE", "/highlights/id-1", false, |d| Box::pin(d.highlights().delete("id-1"))),
    ("highlights.list", "GET", "/highlights", false, |d| Box::pin(d.highlights().list(None))),
    ("highlights.removeStory", "DELETE", "/highlights/id-1/items", true, |d| Box::pin(d.highlights().remove_story("id-1", Some(json!({"probe": true}))))),
    ("highlights.update", "PATCH", "/highlights/id-1", true, |d| Box::pin(d.highlights().update("id-1", Some(json!({"probe": true}))))),
    ("notifications.getPreferences", "GET", "/notifications/preferences", false, |d| Box::pin(d.notifications().get_preferences())),
    ("notifications.list", "GET", "/notifications", false, |d| Box::pin(d.notifications().list(None))),
    ("notifications.markAllRead", "PATCH", "/notifications", true, |d| Box::pin(d.notifications().mark_all_read(Some(json!({"probe": true}))))),
    ("notifications.markRead", "PATCH", "/notifications/id-1", true, |d| Box::pin(d.notifications().mark_read("id-1", Some(json!({"probe": true}))))),
    ("notifications.updatePreferences", "PATCH", "/notifications/preferences", true, |d| Box::pin(d.notifications().update_preferences(Some(json!({"probe": true}))))),
    ("organizations.acceptInvitation", "POST", "/invitations/accept", true, |d| Box::pin(d.organizations().accept_invitation(Some(json!({"probe": true}))))),
    ("organizations.create", "POST", "/organizations", true, |d| Box::pin(d.organizations().create(Some(json!({"probe": true}))))),
    ("organizations.createInvitation", "POST", "/organizations/id-1/invitations", true, |d| Box::pin(d.organizations().create_invitation("id-1", Some(json!({"probe": true}))))),
    ("organizations.list", "GET", "/organizations", false, |d| Box::pin(d.organizations().list())),
    ("organizations.listInvitations", "GET", "/organizations/id-1/invitations", false, |d| Box::pin(d.organizations().list_invitations("id-1"))),
    ("organizations.listMembers", "GET", "/organizations/id-1/members", false, |d| Box::pin(d.organizations().list_members("id-1"))),
    ("organizations.removeMember", "DELETE", "/organizations/id-1/members?userId=userId-v", false, |d| Box::pin(d.organizations().remove_member("id-1", "userId-v"))),
    ("organizations.revokeInvitation", "DELETE", "/organizations/id-1/invitations?invitationId=invitationId-v", false, |d| Box::pin(d.organizations().revoke_invitation("id-1", "invitationId-v"))),
    ("organizations.updateMember", "PATCH", "/organizations/id-1/members", true, |d| Box::pin(d.organizations().update_member("id-1", Some(json!({"probe": true}))))),
    ("posts.create", "POST", "/posts", true, |d| Box::pin(d.posts().create(Some(json!({"probe": true}))))),
    ("posts.createComment", "POST", "/posts/id-1/comments", true, |d| Box::pin(d.posts().create_comment("id-1", Some(json!({"probe": true}))))),
    ("posts.get", "GET", "/posts/id-1", false, |d| Box::pin(d.posts().get("id-1"))),
    ("posts.like", "POST", "/posts/id-1/like", false, |d| Box::pin(d.posts().like("id-1"))),
    ("posts.list", "GET", "/posts", false, |d| Box::pin(d.posts().list(None))),
    ("posts.listComments", "GET", "/posts/id-1/comments", false, |d| Box::pin(d.posts().list_comments("id-1", None))),
    ("posts.listSaved", "GET", "/me/saved", false, |d| Box::pin(d.posts().list_saved(None))),
    ("posts.save", "POST", "/posts/id-1/save", true, |d| Box::pin(d.posts().save("id-1", Some(json!({"probe": true}))))),
    ("posts.shareToDm", "POST", "/posts/id-1/share-dm", true, |d| Box::pin(d.posts().share_to_dm("id-1", Some(json!({"probe": true}))))),
    ("posts.unlike", "DELETE", "/posts/id-1/like", false, |d| Box::pin(d.posts().unlike("id-1"))),
    ("posts.unsave", "DELETE", "/posts/id-1/save", false, |d| Box::pin(d.posts().unsave("id-1"))),
    ("posts.update", "PATCH", "/posts/id-1", true, |d| Box::pin(d.posts().update("id-1", Some(json!({"probe": true}))))),
    ("profile.block", "POST", "/blocks/handle-1", false, |d| Box::pin(d.profile().block("handle-1"))),
    ("profile.follow", "POST", "/follow/handle-1", true, |d| Box::pin(d.profile().follow("handle-1", Some(json!({"probe": true}))))),
    ("profile.get", "GET", "/profile/handle-1", false, |d| Box::pin(d.profile().get("handle-1"))),
    ("profile.getHighlight", "GET", "/profile/handle-1/highlights/id-1", false, |d| Box::pin(d.profile().get_highlight("handle-1", "id-1"))),
    ("profile.listBlocks", "GET", "/blocks", false, |d| Box::pin(d.profile().list_blocks())),
    ("profile.listConnections", "GET", "/profile/handle-1/list-1", false, |d| Box::pin(d.profile().list_connections("handle-1", "list-1", None))),
    ("profile.listHighlights", "GET", "/profile/handle-1/highlights", false, |d| Box::pin(d.profile().list_highlights("handle-1"))),
    ("profile.mute", "POST", "/mutes/handle-1", true, |d| Box::pin(d.profile().mute("handle-1", Some(json!({"probe": true}))))),
    ("profile.unblock", "DELETE", "/blocks/handle-1", false, |d| Box::pin(d.profile().unblock("handle-1"))),
    ("profile.unfollow", "DELETE", "/follow/handle-1", false, |d| Box::pin(d.profile().unfollow("handle-1"))),
    ("profile.unmute", "DELETE", "/mutes/handle-1", false, |d| Box::pin(d.profile().unmute("handle-1"))),
    ("projects.addCollaborator", "POST", "/projects/id-1/collaborators", true, |d| Box::pin(d.projects().add_collaborator("id-1", Some(json!({"probe": true}))))),
    ("projects.archive", "POST", "/projects/id-1/archive", true, |d| Box::pin(d.projects().archive("id-1", Some(json!({"probe": true}))))),
    ("projects.buildStatus", "GET", "/projects/id-1/build-status", false, |d| Box::pin(d.projects().build_status("id-1"))),
    ("projects.cancelBuild", "POST", "/projects/id-1/cancel-build", false, |d| Box::pin(d.projects().cancel_build("id-1"))),
    ("projects.checkSubdomain", "GET", "/projects/id-1/subdomain/check?value=value-v", false, |d| Box::pin(d.projects().check_subdomain("id-1", "value-v"))),
    ("projects.create", "POST", "/projects", true, |d| Box::pin(d.projects().create(Some(json!({"probe": true}))))),
    ("projects.createConversation", "POST", "/projects/id-1/conversations", false, |d| Box::pin(d.projects().create_conversation("id-1"))),
    ("projects.delete", "DELETE", "/projects/id-1", false, |d| Box::pin(d.projects().delete("id-1"))),
    ("projects.deleteEnv", "DELETE", "/projects/id-1/env?key=key-v", false, |d| Box::pin(d.projects().delete_env("id-1", "key-v"))),
    ("projects.deleteSkills", "DELETE", "/projects/id-1/skills?slug=slug-v", false, |d| Box::pin(d.projects().delete_skills("id-1", "slug-v"))),
    ("projects.deploy", "POST", "/projects/id-1/deploy", true, |d| Box::pin(d.projects().deploy("id-1", Some(json!({"probe": true}))))),
    ("projects.duplicate", "POST", "/projects/id-1/duplicate", false, |d| Box::pin(d.projects().duplicate("id-1"))),
    ("projects.export", "GET", "/projects/id-1/export", false, |d| Box::pin(d.projects().export("id-1"))),
    ("projects.get", "GET", "/projects/id-1", false, |d| Box::pin(d.projects().get("id-1"))),
    ("projects.getBackend", "GET", "/projects/id-1/backend", false, |d| Box::pin(d.projects().get_backend("id-1"))),
    ("projects.getDeployment", "GET", "/projects/id-1/deploy", false, |d| Box::pin(d.projects().get_deployment("id-1"))),
    ("projects.getDomain", "GET", "/projects/id-1/domain", false, |d| Box::pin(d.projects().get_domain("id-1"))),
    ("projects.getEnv", "GET", "/projects/id-1/env", false, |d| Box::pin(d.projects().get_env("id-1"))),
    ("projects.getKnowledge", "GET", "/projects/id-1/knowledge", false, |d| Box::pin(d.projects().get_knowledge("id-1"))),
    ("projects.getSkills", "GET", "/projects/id-1/skills", false, |d| Box::pin(d.projects().get_skills("id-1"))),
    ("projects.getTools", "GET", "/projects/id-1/tools", false, |d| Box::pin(d.projects().get_tools("id-1"))),
    ("projects.importArtifact", "POST", "/projects/id-1/import-artifact", true, |d| Box::pin(d.projects().import_artifact("id-1", Some(json!({"probe": true}))))),
    ("projects.list", "GET", "/projects", false, |d| Box::pin(d.projects().list(None))),
    ("projects.listCheckpoints", "GET", "/projects/id-1/checkpoints", false, |d| Box::pin(d.projects().list_checkpoints("id-1"))),
    ("projects.listCollaborators", "GET", "/projects/id-1/collaborators", false, |d| Box::pin(d.projects().list_collaborators("id-1"))),
    ("projects.listConversations", "GET", "/projects/id-1/conversations", false, |d| Box::pin(d.projects().list_conversations("id-1", None))),
    ("projects.listFiles", "GET", "/projects/id-1/files", false, |d| Box::pin(d.projects().list_files("id-1"))),
    ("projects.provisionBackend", "POST", "/projects/id-1/backend", true, |d| Box::pin(d.projects().provision_backend("id-1", Some(json!({"probe": true}))))),
    ("projects.recommendDomains", "GET", "/projects/id-1/domains/recommend", false, |d| Box::pin(d.projects().recommend_domains("id-1"))),
    ("projects.removeCollaborator", "DELETE", "/projects/id-1/collaborators?collaboratorId=collaboratorId-v", false, |d| Box::pin(d.projects().remove_collaborator("id-1", "collaboratorId-v"))),
    ("projects.removeDomain", "DELETE", "/projects/id-1/domain", false, |d| Box::pin(d.projects().remove_domain("id-1"))),
    ("projects.restore", "POST", "/projects/id-1", false, |d| Box::pin(d.projects().restore("id-1"))),
    ("projects.setDomain", "POST", "/projects/id-1/domain", true, |d| Box::pin(d.projects().set_domain("id-1", Some(json!({"probe": true}))))),
    ("projects.undeploy", "DELETE", "/projects/id-1/deploy", false, |d| Box::pin(d.projects().undeploy("id-1"))),
    ("projects.update", "PATCH", "/projects/id-1", true, |d| Box::pin(d.projects().update("id-1", Some(json!({"probe": true}))))),
    ("projects.updateEnv", "PUT", "/projects/id-1/env", true, |d| Box::pin(d.projects().update_env("id-1", Some(json!({"probe": true}))))),
    ("projects.updateKnowledge", "PUT", "/projects/id-1/knowledge", true, |d| Box::pin(d.projects().update_knowledge("id-1", Some(json!({"probe": true}))))),
    ("projects.updateSkills", "PUT", "/projects/id-1/skills", true, |d| Box::pin(d.projects().update_skills("id-1", Some(json!({"probe": true}))))),
    ("projects.updateSubdomain", "PATCH", "/projects/id-1/subdomain", true, |d| Box::pin(d.projects().update_subdomain("id-1", Some(json!({"probe": true}))))),
    ("projects.updateTools", "PUT", "/projects/id-1/tools", true, |d| Box::pin(d.projects().update_tools("id-1", Some(json!({"probe": true}))))),
    ("projects.writeFiles", "POST", "/projects/id-1/files", true, |d| Box::pin(d.projects().write_files("id-1", Some(json!({"probe": true}))))),
    ("rag.clear", "DELETE", "/rag?projectId=projectId-v", false, |d| Box::pin(d.rag().clear("projectId-v"))),
    ("rag.index", "POST", "/rag", true, |d| Box::pin(d.rag().index(Some(json!({"probe": true}))))),
    ("rag.search", "GET", "/rag?projectId=projectId-v", false, |d| Box::pin(d.rag().search("projectId-v", None))),
    ("reports.create", "POST", "/reports", true, |d| Box::pin(d.reports().create(Some(json!({"probe": true}))))),
    ("search.code", "GET", "/search", false, |d| Box::pin(d.search().code(None))),
    ("search.web", "POST", "/v1/search", true, |d| Box::pin(d.search().web(Some(json!({"probe": true}))))),
    ("skills.get", "GET", "/skills/misarai/slug-1", false, |d| Box::pin(d.skills().get("slug-1"))),
    ("skills.list", "GET", "/skills/misarai", false, |d| Box::pin(d.skills().list())),
    ("social.dismissSuggestion", "POST", "/social/suggestions/handle-1/dismiss", false, |d| Box::pin(d.social().dismiss_suggestion("handle-1"))),
    ("social.listSuggestions", "GET", "/social/suggestions", false, |d| Box::pin(d.social().list_suggestions(None))),
    ("social.meMutuals", "GET", "/social/me/mutuals", false, |d| Box::pin(d.social().me_mutuals(None))),
    ("social.relationship", "GET", "/social/relationship/handle-1", false, |d| Box::pin(d.social().relationship("handle-1"))),
    ("stories.create", "POST", "/stories", true, |d| Box::pin(d.stories().create(Some(json!({"probe": true}))))),
    ("stories.delete", "DELETE", "/stories/id-1", false, |d| Box::pin(d.stories().delete("id-1"))),
    ("stories.get", "GET", "/stories/id-1", false, |d| Box::pin(d.stories().get("id-1"))),
    ("stories.listMyPosts", "GET", "/stories/my-posts", false, |d| Box::pin(d.stories().list_my_posts(None))),
    ("stories.listViewers", "GET", "/stories/id-1/viewers", false, |d| Box::pin(d.stories().list_viewers("id-1", None))),
    ("stories.markSeen", "POST", "/stories/id-1/view", false, |d| Box::pin(d.stories().mark_seen("id-1"))),
    ("stories.reply", "POST", "/stories/id-1/reply", true, |d| Box::pin(d.stories().reply("id-1", Some(json!({"probe": true}))))),
    ("stories.tray", "GET", "/stories/tray", false, |d| Box::pin(d.stories().tray())),
    ("tags.get", "GET", "/tags/tag-1", false, |d| Box::pin(d.tags().get("tag-1", None))),
    ("templates.create", "POST", "/templates", true, |d| Box::pin(d.templates().create(Some(json!({"probe": true}))))),
    ("templates.delete", "DELETE", "/templates/id-1", false, |d| Box::pin(d.templates().delete("id-1"))),
    ("templates.get", "GET", "/templates/id-1", false, |d| Box::pin(d.templates().get("id-1"))),
    ("templates.list", "GET", "/templates", false, |d| Box::pin(d.templates().list(None))),
    ("templates.remix", "POST", "/templates/remix", true, |d| Box::pin(d.templates().remix(Some(json!({"probe": true}))))),
    ("templates.update", "PATCH", "/templates/id-1", true, |d| Box::pin(d.templates().update("id-1", Some(json!({"probe": true}))))),
    ("templates.use", "POST", "/templates/id-1/use", true, |d| Box::pin(d.templates().use_("id-1", Some(json!({"probe": true}))))),
    ("usage.list", "GET", "/usage", false, |d| Box::pin(d.usage().list(None))),
    ("usage.plan", "GET", "/usage/plan", false, |d| Box::pin(d.usage().plan())),
    ("usage.track", "POST", "/usage/track", true, |d| Box::pin(d.usage().track(Some(json!({"probe": true}))))),
];

pub(crate) const STREAMS: &[(&str, &str, &str, bool, StreamCall)] = &[
    ("chat.studioStream", "POST", "/chat", true, |d| Box::pin(d.chat().studio_stream(Some(json!({"probe": true}))))),
    ("chat.completionsStream", "POST", "/v1/chat/completions", true, |d| Box::pin(d.chat().completions_stream(Some(json!({"probe": true}))))),
    ("projects.buildStream", "GET", "/projects/id-1/build-stream", false, |d| Box::pin(d.projects().build_stream("id-1"))),
];

fn client(server: &MockServer) -> MisarDev {
    MisarDev::builder()
        .api_key("mdk_test_key")
        .base_url(format!("{}/dev", server.uri()))
        .build()
        .expect("client")
}

fn url_of(req: &wiremock::Request) -> String {
    match req.url.query() {
        Some(q) => format!("{}?{}", req.url.path(), q),
        None => req.url.path().to_owned(),
    }
}

#[test]
fn table_covers_every_operation() {
    assert_eq!(OPERATIONS.len() + STREAMS.len(), super::OPERATION_COUNT);
}

#[tokio::test]
async fn every_generated_method_builds_its_route() {
    let server = MockServer::start().await;
    for (name, verb, url, has_body, call) in OPERATIONS {
        server.reset().await;
        Mock::given(any())
            .respond_with(ResponseTemplate::new(200).set_body_json(json!({"ok": true})))
            .mount(&server)
            .await;
        let dev = client(&server);
        let out = call(&dev).await.unwrap_or_else(|e| panic!("{name}: {e}"));
        assert_eq!(out, json!({"ok": true}), "{name}");
        let reqs = server.received_requests().await.expect("recording");
        assert_eq!(reqs.len(), 1, "{name}");
        assert_eq!(reqs[0].method.as_str(), *verb, "{name}");
        assert_eq!(url_of(&reqs[0]), format!("/dev{url}"), "{name}");
        if *has_body {
            let body: Value = serde_json::from_slice(&reqs[0].body).expect("json body");
            assert_eq!(body["probe"], json!(true), "{name}");
        }
    }
}

#[tokio::test]
async fn every_generated_stream_builds_its_route() {
    let server = MockServer::start().await;
    for (name, verb, url, has_body, call) in STREAMS {
        server.reset().await;
        Mock::given(any())
            .respond_with(
                ResponseTemplate::new(200)
                    .insert_header("content-type", "text/event-stream")
                    .set_body_string("data: {\"x\":1}\n\n"),
            )
            .mount(&server)
            .await;
        let dev = client(&server);
        let mut stream = call(&dev).await.unwrap_or_else(|e| panic!("{name}: {e}"));
        let mut n = 0;
        while let Some(ev) = stream.next().await {
            assert_eq!(ev.expect("event").data, json!({"x": 1}), "{name}");
            n += 1;
        }
        assert_eq!(n, 1, "{name}");
        let reqs = server.received_requests().await.expect("recording");
        assert_eq!(reqs[0].method.as_str(), *verb, "{name}");
        assert_eq!(url_of(&reqs[0]), format!("/dev{url}"), "{name}");
        if *has_body {
            let body: Value = serde_json::from_slice(&reqs[0].body).expect("json body");
            assert_eq!(body["probe"], json!(true), "{name}");
        }
    }
}