Apache Iggyのコネクター変更を複数の専門視点から厳しくレビューし、指摘ごとに独立した検証を行って、修正が必要な問題を整理します。
- コネクターのPRをマージ前に確認したいとき
- ブランチの変更を専門視点でレビューしたいとき
- 指摘の妥当性を独立して検証したいとき
Apache Iggyコネクターのメッセージにフィールド追加・削除や形式変換などの処理を加える変換機能を設計し、設定連携やテストまで整えます。
Add a new transform under `core/connectors/sdk/src/transforms/`. Transforms mutate, filter, or convert messages between source/encode and decode/sink. Use when adding field-level transforms (add/delete/update/filter), format conversions, envelope unwrapping, or similar config-driven message manipulations for Apache Iggy connectors. Use for transform authoring. NOT for runtime internals or sink/source loops.
実行スコア 未検証
インストール方法を見るApache Iggy Connectors SDKにメッセージ変換処理を追加する手順を案内します。変換処理はシンク側ではデコード後、ソース側ではエンコード前に実行され、設定に応じてフィールドを追加・削除・更新したり、条件に合うメッセージを除外したりできます。形式変換やネストされたデータの展開にも対応する実装例を示します。変換型の登録、設定ファクトリーへの接続、READMEの使用例、正常系・通過・除外・不正設定のテストまで扱います。設定検証や正規表現などの準備は初期化時に行い、メッセージ処理時の無駄な複製を避ける考え方も説明します。
コネクター間でメッセージの形を整えたいときや、不要なメッセージを条件で除きたいときに使えます。既存の変換処理と同じ構成で新しい変換機能をSDKに追加したい開発者にも向いています。
変換処理は同期実行で、初期化後は不変の状態で共有されます。ペイロードの形を変える場合は、接続先への影響を確認します。
この紹介文は、公開されている SKILL.md をもとに AI(OpenAI Codex)が作成しました。正確な仕様は下の原文を確認してください。
インストールする前に、エージェントに与えられる指示の中身を確認できます。
A transform implements iggy_connector_sdk::transforms::Transform. It
runs after decode (for sinks) or before encode (for sources), in a chain
configured per-connector. Config-driven, composable.
Universal connector rules live in connectors-overview. This skill covers transform authoring only.
Transform trait signature (sync vs async, return type) - cascades to every transform.Arc-cloned across tasks. mutation requires interior locking and is rarely what you actually want.sdk/src/transforms/
├── mod.rs Transform trait, TransformType enum, from_config() factory,
│ FieldValue + ComputedValue helpers
├── add_fields.rs AddFields - append static or computed fields to JSON
├── delete_fields.rs DeleteFields - remove fields by key
├── update_fields.rs UpdateFields - conditional field replacement
├── filter_fields.rs FilterFields - keep/drop messages by key/value pattern
├── unwrap_envelope.rs UnwrapEnvelope - promote nested field to top-level payload
├── proto_convert.rs ProtoConvert - bidirectional Proto ⇄ Json/Text/Raw/etc.
├── flatbuffer_convert.rs FlatBufferConvert
├── avro_convert.rs AvroConvert
└── json/ Shared JSON helpers (path traversal, ops)
// sdk/src/transforms/mod.rs - Transform
pub trait Transform: Send + Sync {
fn r#type(&self) -> TransformType;
fn transform(
&self,
metadata: &TopicMetadata,
message: DecodedMessage,
) -> Result<Option<DecodedMessage>, Error>;
}
Semantics:
&self - transforms are immutable post-construction. Internal state (compiled regex, parsed paths, schema descriptors) is computed in new() and stored as plain struct fields. No locks needed - the whole instance is shared via Arc<dyn Transform>.message by value - the transform owns the DecodedMessage and may mutate freely without cloning.Ok(Some(msg)) to pass through (possibly modified), Ok(None) to filter (drop silently), Err(e) for actual failures.&TopicMetadata is for routing-aware logic. Most transforms ignore it.sdk/src/transforms/my_transform.rs. Apache 2.0 license header. Code reads top to bottom - config struct, then transform struct, then impl, then helpers.
/* Apache 2.0 header */
use crate::transforms::{Transform, TransformType};
use crate::{DecodedMessage, Error, Payload, TopicMetadata};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MyTransformConfig {
pub key: String,
pub option: Option<bool>,
}
pub struct MyTransform {
config: MyTransformConfig,
}
impl MyTransform {
pub fn new(config: MyTransformConfig) -> Result<Self, Error> {
if config.key.is_empty() {
return Err(Error::InvalidConfigValue(
"my_transform.key must not be empty".into(),
));
}
Ok(Self { config })
}
}
impl Transform for MyTransform {
fn r#type(&self) -> TransformType {
TransformType::MyTransform
}
fn transform(
&self,
_metadata: &TopicMetadata,
mut message: DecodedMessage,
) -> Result<Option<DecodedMessage>, Error> {
// Most transforms operate only on Payload::Json - non-JSON is a no-op.
let Payload::Json(ref mut value) = message.payload else {
return Ok(Some(message));
};
// Mutate `value` in place via simd_json::OwnedValue API.
// Return Ok(None) to filter, Ok(Some(message)) to pass through.
Ok(Some(message))
}
}
transforms/mod.rsThree edits, all required:
(a) Module declaration + re-export at the top:
mod my_transform;
pub use my_transform::{MyTransform, MyTransformConfig};
(b) Discriminant in TransformType:
pub enum TransformType {
AddFields,
// ...
MyTransform, // snake_case form in TOML: "my_transform"
}
(c) Branch in from_config():
TransformType::MyTransform => {
// For new transforms, prefer Error::InvalidConfigValue with formatted context -
// it preserves the underlying serde_json error. Existing code uses bare
// Error::InvalidConfig for most variants (UnwrapEnvelope is the exception).
let cfg: MyTransformConfig = serde_json::from_value(raw.clone())
.map_err(|error| Error::InvalidConfigValue(format!("my_transform: {error}")))?;
Ok(Arc::new(MyTransform::new(cfg)?))
}
from_config() returns Arc<dyn Transform> so the same instance is shared across the pipeline.
Add a TOML usage example to sdk/README.md:
[transforms.my_transform]
enabled = true
key = "some_field"
option = true
Bottom of my_transform.rs:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn given_matching_field_should_transform() { ... }
#[test]
fn given_non_json_payload_should_passthrough() { ... }
#[test]
fn given_filter_condition_should_drop_message() { /* assert Ok(None) */ }
#[test]
fn given_invalid_config_should_error_in_new() {
let cfg = MyTransformConfig { key: "".into(), option: None };
assert!(matches!(MyTransform::new(cfg), Err(Error::InvalidConfigValue(_))));
}
fn topic_meta() -> TopicMetadata {
TopicMetadata { stream: "s".into(), topic: "t".into() }
}
fn json_message(json: serde_json::Value) -> DecodedMessage { /* ... */ }
}
Most field-level transforms only make sense for Payload::Json. Convention (see add_fields.rs::AddFields::transform): silently pass through non-JSON:
let Payload::Json(ref mut value) = message.payload else {
return Ok(Some(message));
};
Format-conversion transforms (proto_convert, avro_convert, flatbuffer_convert) are the exception - they explicitly convert between variants.
OwnedValue in place, don't clone-then-replacesimd_json::OwnedValue is mutable in place:
if let OwnedValue::Object(ref mut obj) = value {
obj.insert(key.into(), new_value);
}
Do NOT clone the value, mutate the clone, then message.payload = Payload::Json(new). The whole point of &mut value is in-place mutation.
Ok(None) drops the message silently - no error log, no metric. By design (filters succeed when they decide to drop). For visibility, log at debug! level.
Validate in new() and return Err(Error::InvalidConfigValue(...)). Don't panic!. Don't validate on the hot path.
For transforms with regex or compiled patterns, compile once in new() and store the compiled form.
Transforms are constructed once at startup and Arc-cloned across tasks. Must be Send + Sync. No mutable runtime state - if you need accumulator-like behavior, that's not a Transform, that's a sink.
Things derived from config (compiled regex, parsed paths, schema descriptors) should be computed in new() and stored as plain struct fields. The whole instance is shared via Arc<dyn Transform>, so per-field locking is unnecessary. See sdk/src/transforms/filter_fields.rs::FilterFields::new for the canonical pattern: regexes compile in new(), get stored as struct fields, are then read via &self.
transform() is syncThe trait method is sync (not async). Don't propose making it async - that would require pervasive change across the runtime. Schemas and other external resources should be loaded in new() (potentially via a fallible try_new), not lazily during transform().
| Scenario | Variant |
|---|---|
Config validation in new() | Error::InvalidConfigValue("transform_name: ...") |
| Per-message validation (bad shape) | Error::InvalidRecordValue("...") |
| Schema resolution failure | Error::InvalidTransformer |
| Format conversion failure | Error::InvalidPayloadType |
Returning Err from transform() stops the message and is logged. For non-fatal field-level issues (field missing when transform expects it), prefer pass-through (Ok(Some(message))) unless config explicitly says to fail.
mod.rs edits - module declaration, TransformType variant, from_config() branch.#[serde(rename_all = "snake_case")] mismatch between enum variant name and TOML key.OwnedValue when in-place mutation would work.transform() instead of new().std::sync::Mutex for internal state on the hot path - precompute in new() and store as plain struct fields. Transforms run on every message. locks are wasted work.Err for a filter - filters use Ok(None).cargo fmt --all
cargo sort --no-format --workspace
cargo clippy -p iggy_connector_sdk --all-targets --all-features -- -D warnings
cargo test -p iggy_connector_sdk --all-features
Verify by adding the transform to core/connectors/runtime/example_config/connectors/ and running the runtime - the transform should apply to each message.
If the transform changes payload shape sufficiently to break downstream sinks (e.g., renaming primary keys), call it out in the PR description.
Discussion / help: see AGENTS.md.
まだレビューはありません。使ってみた感想をお寄せください。
ログインなしで書き込めます。使ってみた感想、質問、動かなかった環境などを気軽にどうぞ。
まだコメントはありません。最初のコメントを書いてみませんか。
Apache Iggyのコネクター変更を複数の専門視点から厳しくレビューし、指摘ごとに独立した検証を行って、修正が必要な問題を整理します。
Apache Iggyとコネクタープラグインをつなぐランタイムの開発に使います。読み込みやデータ転送、状態保存、設定、HTTP API、メトリクスなどの変更箇所を把握できます。
Apache Iggy Connectors SDKのスキーマ、エンコーダー、デコーダー、エラー型やFFIなどを追加・変更するときに、関連箇所と互換性への影響を整理するスキルです。
Apache Iggyのストリームからメッセージを受け取り、データベースやHTTPなど外部システムへ送るSinkプラグインの作成・変更を支援します。
データベース、API、キューなど外部システムをポーリングし、Apache Iggyのストリームへメッセージを送るSourceプラグインの作成・変更を支援します。
Apache Iggyのコネクター向けに、ユニットテストや実サービスを使う統合テストを設計・作成し、命名規則や状態復元などのテスト慣例に沿って確認します。