mocra 0.3.0

A distributed, event-driven crawling and data collection framework
# 开发文档

本文展示如何使用当前公共 API 开发爬虫模块。

## 最小模块

```rust
use std::sync::Arc;

use async_trait::async_trait;
use mocra::common::interface::module::ToSyncBoxStream;
use mocra::common::interface::{ModuleNodeTrait, ModuleTrait, SyncBoxStream};
use mocra::common::model::{NodeParseOutput, Request, Response};
use mocra::common::interface::module::{NodeGenerateContext, NodeParseContext};
use mocra::errors::Result;

struct MyModule;

#[async_trait]
impl ModuleTrait for MyModule {
    fn name(&self) -> &'static str {
        "my_module"
    }

    fn version(&self) -> i32 {
        1
    }

    fn default_arc() -> Arc<dyn ModuleTrait>
    where
        Self: Sized,
    {
        Arc::new(Self)
    }

    async fn add_step(&self) -> Vec<Arc<dyn ModuleNodeTrait>> {
        vec![Arc::new(ListNode)]
    }
}
```

## 最小节点

```rust
struct ListNode;

#[async_trait]
impl ModuleNodeTrait for ListNode {
    async fn generate(&self, _ctx: NodeGenerateContext<'_>) -> Result<SyncBoxStream<'static, Request>> {
        vec![Request::new("https://example.com", "GET")].into_stream_ok()
    }

    async fn parser(&self, _response: Response, _ctx: NodeParseContext<'_>) -> Result<NodeParseOutput> {
        Ok(NodeParseOutput::default().finish())
    }

    fn stable_node_key(&self) -> &'static str {
        "list"
    }
}
```

## 运行 Engine

```rust
use std::sync::Arc;

use mocra::common::state::State;
use mocra::engine::Engine;

#[tokio::main]
async fn main() -> mocra::errors::Result<()> {
    let state = Arc::new(State::new("config.toml").await);
    let engine = Engine::new(Arc::clone(&state), None).await?;

    engine.register_module(MyModule::default_arc()).await;
    engine.start().await;

    Ok(())
}
```

请在调用 `start()` 前注册模块和中间件。

## 请求生成

`generate()` 返回 `SyncBoxStream<'static, Request>`。如果请求数量较小,可以使用 prelude 中的 `Vec<Request>::into_stream_ok()`。

```rust
let request = Request::new("https://example.com/api", "POST")
    .with_json(&serde_json::json!({ "page": 1 }))
    .enable_response_cache(true);

vec![request].into_stream_ok()
```

分页游标、业务 ID 或 parser 路由提示可以放入 request metadata。

## 响应解析

parser 可以输出数据、继续到另一个节点或结束:

```rust
let input = build_next_node_input();

Ok(NodeParseOutput::default()
    .with_next(NodeDispatch::new("detail", input))
    .finish())
```

下游节点需要类型化输入时使用 `with_next(...)`。当前分支完成时使用 `finish()`。

## DAG 节点

生产工作流中的节点应实现 `stable_node_key()`:

```rust
fn stable_node_key(&self) -> &'static str {
    "detail"
}
```

该 key 必须在同一个模块 DAG 中唯一。

## 模块选项

通过模块方法定义通用行为:

```rust
fn rate_limit(&self) -> Option<f32> {
    Some(2.0)
}

fn response_cache_enabled(&self) -> bool {
    true
}

fn response_cache_ttl_secs(&self) -> Option<u64> {
    Some(3600)
}
```

## Cron 调度

从 `cron()` 返回 `CronConfig` 可以启用定时启动。临时执行可以通过 HTTP API 或应用配置的队列路径分发任务。

## 模块测试

推荐覆盖:

- 编译模块 DAG;
- 运行 `generate()` 并断言请求;
- 使用 fixture response 运行 `parser()` 并断言 `NodeParseOutput`- 验证 `stable_node_key()` 在模块内唯一;
- 执行 `cargo check` 确认 API 兼容。