diff --git a/README.md b/README.md index 55db649..89f939a 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,8 @@ When `requestId` is `null`, the message is treated as standalone and won't be ma The existing `String`/JSON methods remain unchanged. To inspect protobuf payloads, create an interceptor with a decoder supplied by your app: +There is no protobuf feature flag or `DariConfig` toggle. Using this interceptor overload and the protobuf capture methods opts the bridge into protobuf inspection. See the [full guide](https://easyhooon.github.io/dari/docs/protobuf) or [Korean guide](https://easyhooon.github.io/dari/ko/docs/protobuf). + ```kotlin import com.easyhooon.dari.interceptor.PayloadPart.REQUEST import com.easyhooon.dari.interceptor.PayloadPart.RESPONSE diff --git a/documentation/content/docs/interceptor.mdx b/documentation/content/docs/interceptor.mdx index d6bcf6f..f5b3aea 100644 --- a/documentation/content/docs/interceptor.mdx +++ b/documentation/content/docs/interceptor.mdx @@ -50,34 +50,7 @@ Use `Dari.createInterceptor()` in app code so release builds compile against `da ## Protocol Buffers (optional) -The string/JSON interface remains the default. For protobuf payloads, provide a decoder when creating the interceptor: - -```kotlin -val protobufInterceptor = Dari.createInterceptor( - tag = "OrderBridge", - protobufDecoder = ProtobufPayloadDecoder { payload, context -> - when (context.handlerName to context.part) { - "createOrder" to PayloadPart.REQUEST -> - JsonFormat.printer().print(CreateOrderRequest.parseFrom(payload)) - "createOrder" to PayloadPart.RESPONSE -> - JsonFormat.printer().print(CreateOrderResponse.parseFrom(payload)) - else -> null - } - }, -) - -protobufInterceptor?.onWebToAppProtobufRequest( - handlerName = "createOrder", - requestId = requestId, - requestData = protobufBytes, -) -``` - -The decoder belongs to the consuming app, so Dari does not require a specific protobuf runtime. Returning `null` marks the decoder as unavailable for that handler. Decode failures are captured without crashing message inspection. Decoding runs synchronously on the interceptor caller's thread, so keep decoder work bounded. - -Base64 is not required by Dari. Decode Base64 before calling the interceptor when using a string-only bridge, or pass bytes received from an `ArrayBuffer` bridge directly. - -The REQUEST and RESPONSE tabs offer `Decoded` and `Raw` views. Dari retains at most the first 4 KB of each protobuf payload for the raw preview and displays it as Hex and Base64 while preserving the original byte size and truncation state. The displayed Base64 is generated for inspection and does not imply that the bridge used Base64 transport. +Protobuf inspection has no feature flag and does not replace the string/JSON API. See the [Protocol Buffers guide](./protobuf) for activation requirements, decoder setup, bridge transport options, and all request/response methods. ## Methods diff --git a/documentation/content/docs/ko/interceptor.mdx b/documentation/content/docs/ko/interceptor.mdx index 453a24a..51c22f3 100644 --- a/documentation/content/docs/ko/interceptor.mdx +++ b/documentation/content/docs/ko/interceptor.mdx @@ -50,34 +50,7 @@ val interceptor = Dari.createInterceptor(tag = "PaymentBridge") ## Protocol Buffers (선택 사항) -기존 String/JSON 인터페이스는 그대로 유지됩니다. protobuf 페이로드를 검사하려면 인터셉터를 생성할 때 앱의 decoder를 전달합니다: - -```kotlin -val protobufInterceptor = Dari.createInterceptor( - tag = "OrderBridge", - protobufDecoder = ProtobufPayloadDecoder { payload, context -> - when (context.handlerName to context.part) { - "createOrder" to PayloadPart.REQUEST -> - JsonFormat.printer().print(CreateOrderRequest.parseFrom(payload)) - "createOrder" to PayloadPart.RESPONSE -> - JsonFormat.printer().print(CreateOrderResponse.parseFrom(payload)) - else -> null - } - }, -) - -protobufInterceptor?.onWebToAppProtobufRequest( - handlerName = "createOrder", - requestId = requestId, - requestData = protobufBytes, -) -``` - -decoder는 사용하는 앱이 제공하므로 Dari는 특정 protobuf runtime에 의존하지 않습니다. 해당 handler를 지원하지 않으면 `null`을 반환할 수 있으며, 디코딩 실패가 메시지 캡처를 중단시키지 않습니다. 디코딩은 인터셉터 호출 스레드에서 동기 실행되므로 작업량을 제한해야 합니다. - -Base64는 Dari의 필수 조건이 아닙니다. 문자열 전용 브릿지에서는 Base64를 먼저 디코딩하고, `ArrayBuffer` 브릿지에서는 전달받은 bytes를 바로 사용할 수 있습니다. - -REQUEST와 RESPONSE 탭에서는 `Decoded`와 `Raw` 보기를 제공합니다. Dari는 raw preview를 위해 protobuf 페이로드의 앞부분을 최대 4KB까지만 보관하며, 원본 바이트 크기와 잘림 여부를 유지한 채 Hex와 Base64로 표시합니다. 이 Base64는 검사를 위해 Dari가 생성한 표시 형식이며, 실제 브릿지가 Base64로 운반했다는 의미는 아닙니다. +protobuf 검사에는 별도 feature flag가 없으며 기존 String/JSON API를 대체하지 않습니다. 활성화 조건, decoder 설정, 브릿지 전송 방식 및 모든 요청/응답 메서드는 [Protocol Buffers 가이드](./protobuf)를 참고하세요. ## 메서드 diff --git a/documentation/content/docs/ko/meta.json b/documentation/content/docs/ko/meta.json index 4faf407..40738a4 100644 --- a/documentation/content/docs/ko/meta.json +++ b/documentation/content/docs/ko/meta.json @@ -1,4 +1,4 @@ { "title": "Dari", - "pages": ["index", "setup", "configuration", "interceptor", "modules"] + "pages": ["index", "setup", "configuration", "interceptor", "protobuf", "modules"] } diff --git a/documentation/content/docs/ko/protobuf.mdx b/documentation/content/docs/ko/protobuf.mdx new file mode 100644 index 0000000..ede7399 --- /dev/null +++ b/documentation/content/docs/ko/protobuf.mdx @@ -0,0 +1,148 @@ +--- +title: Protocol Buffers +description: 선택적 protobuf 페이로드 검사 활성화 및 연동 방법 +--- + +## 활성화 방법 + +Protocol Buffers 지원은 Dari 1.6.0 이상에서 사용할 수 있습니다. 별도 feature flag나 `DariConfig` 토글은 없습니다. + +인터셉터를 생성하고 호출하는 시점에 선택적으로 활성화합니다: + +1. `ProtobufPayloadDecoder`를 전달해 인터셉터를 생성합니다. +2. protobuf 요청 및 응답 메서드에 원본 `ByteArray`를 전달합니다. + +`ProtobufDariInterceptor`는 `DariInterceptor`를 확장하므로 같은 인터셉터에서 기존 String/JSON 메서드도 계속 사용할 수 있습니다. + +| 조건 | 필요 여부 | 설명 | +| --- | --- | --- | +| Dari 1.6.0 이상 | 필요 | `dari`와 `dari-noop` 버전을 동일하게 사용 | +| `protobufEnabled` 플래그 | 불필요 | protobuf 설정 플래그는 없음 | +| 앱의 protobuf runtime 및 generated message | 필요 | Dari는 runtime을 추가하거나 선택하지 않음 | +| 표시 문자열을 반환하는 decoder | 필요 | 알 수 없는 handler에서는 `null` 반환 가능 | +| Base64 | 문자열 전용 브릿지만 필요 | Dari에는 디코딩된 `ByteArray`를 전달 | +| `.proto` 또는 메시지 필드 변경 | 불필요 | 기존 schema와 wire payload를 그대로 사용 | + +## 1. protobuf 지원 인터셉터 생성 + +schema를 이용한 디코딩은 앱이 담당합니다. handler와 요청/응답 타입에 맞는 사람이 읽을 수 있는 문자열을 반환합니다. 가능하면 JSON을 반환하면 Dari의 Decoded 화면에서 보기 좋게 정리됩니다. + +```kotlin +import com.easyhooon.dari.Dari +import com.easyhooon.dari.interceptor.PayloadPart.REQUEST +import com.easyhooon.dari.interceptor.PayloadPart.RESPONSE +import com.easyhooon.dari.interceptor.ProtobufDariInterceptor +import com.easyhooon.dari.interceptor.ProtobufPayloadDecoder + +val interceptor: ProtobufDariInterceptor? = Dari.createInterceptor( + tag = "OrderBridge", + protobufDecoder = ProtobufPayloadDecoder { payload, context -> + when (context.handlerName to context.part) { + "createOrder" to REQUEST -> + JsonFormat.printer().print(CreateOrderRequest.parseFrom(payload)) + "createOrder" to RESPONSE -> + JsonFormat.printer().print(CreateOrderResponse.parseFrom(payload)) + else -> null + } + }, +) +``` + +Raw 데이터만 검사하려면 decoder가 의도적으로 `null`을 반환해도 됩니다: + +```kotlin +val interceptor = Dari.createInterceptor( + protobufDecoder = ProtobufPayloadDecoder { _, _ -> null }, +) +``` + +이 경우 Dari는 페이로드를 protobuf로 기록하고 decode 상태를 `DECODER_UNAVAILABLE`로 표시하며, raw preview와 원본 바이트 크기는 그대로 보존합니다. + +## 2. 원본 바이트 캡처 + +기존 브릿지의 요청과 응답 처리 지점에서 Dari를 호출합니다. 캡처 전에 페이로드를 JSON으로 변환하지 않습니다. + +```kotlin +interceptor?.onWebToAppProtobufRequest( + handlerName = "createOrder", + requestId = requestId, + requestData = requestBytes, +) + +val response = CreateOrderResponse.newBuilder() + .setOrderId("order-123") + .build() + +interceptor?.onWebToAppProtobufResponse( + handlerName = "createOrder", + requestId = requestId, + responseData = response.toByteArray(), + isSuccess = true, +) +``` + +요청과 응답을 연결하려면 동일하고 안정적인 `requestId`를 사용합니다. 단독 메시지 또는 fire-and-forget인 경우에만 `null`을 전달합니다. + +## 방향별 메서드 + +| 브릿지 이벤트 | Dari 메서드 | +| --- | --- | +| Web이 App에 요청 | `onWebToAppProtobufRequest()` | +| App이 Web에 응답 | `onWebToAppProtobufResponse()` | +| App이 Web에 요청 | `onAppToWebProtobufRequest()` | +| Web이 App에 응답 | `onAppToWebProtobufResponse()` | + +`handlerName`, 방향 및 요청/응답 구분은 `ProtobufDecodeContext`를 통해 decoder에 전달되므로 요청과 응답에 서로 다른 generated type을 사용할 수 있습니다. + +## Base64와 브릿지 전송 방식 + +Base64는 protobuf나 Dari의 필수 조건이 아닙니다. + +- 브릿지가 bytes를 제공하면 Dari에 바로 전달합니다. +- 문자열 전용 브릿지가 Base64를 운반하면 먼저 디코딩한 bytes를 전달합니다. + +```kotlin +@JavascriptInterface +fun onProtobufRequest(requestId: String, base64Data: String) { + val bytes = Base64.decode(base64Data, Base64.NO_WRAP) + interceptor?.onWebToAppProtobufRequest( + handlerName = "createOrder", + requestId = requestId, + requestData = bytes, + ) +} +``` + +Dari의 Raw 화면에 표시되는 Base64는 검사를 위해 생성된 표시 형식이며, 앱이 Base64로 페이로드를 운반했다는 의미가 아닙니다. + +## Dari에 표시되는 내용 + +REQUEST와 RESPONSE 탭에서 두 가지 보기를 제공합니다: + +- **Decoded**: 앱이 제공한 decoder가 반환한 문자열 +- **Raw**: 캡처한 protobuf bytes를 Hex와 Base64로 표시 + +`Decoded`와 `Raw` 선택은 REQUEST와 RESPONSE 탭에서 각각 제공됩니다. 따라서 브릿지 요청과 응답 모두에서 schema로 해석한 값과 실제 wire bytes를 비교할 수 있습니다. + + + + + + + + + + + + + + +
DecodedRaw (Hex 및 Base64)
Dari에서 디코딩된 protobuf 응답Dari에서 Hex와 Base64로 표시된 protobuf 원본 응답
+ +스크린샷은 RESPONSE 탭의 예시이며 REQUEST 탭에서도 동일한 두 가지 보기를 제공합니다. Dari는 원본 바이트 크기와 decode 상태를 기록합니다. Raw 캡처는 앞부분 최대 4KB로 제한되며, 더 큰 페이로드는 잘림 상태로 표시됩니다. 디코딩 실패는 브릿지 통신을 중단시키지 않고 기록됩니다. + +## 런타임 동작 + +- 디코딩은 인터셉터 호출 스레드에서 동기 실행되므로 파싱과 표시 변환 작업을 제한해야 합니다. +- 릴리즈 빌드는 `dari-noop`을 사용하며 `Dari.createInterceptor()`가 `null`을 반환하므로 safe call에는 런타임 오버헤드가 없습니다. +- 동일한 protobuf 지원 인터셉터에서 기존 String/JSON 호출을 계속 사용할 수 있습니다. diff --git a/documentation/content/docs/meta.json b/documentation/content/docs/meta.json index 4faf407..40738a4 100644 --- a/documentation/content/docs/meta.json +++ b/documentation/content/docs/meta.json @@ -1,4 +1,4 @@ { "title": "Dari", - "pages": ["index", "setup", "configuration", "interceptor", "modules"] + "pages": ["index", "setup", "configuration", "interceptor", "protobuf", "modules"] } diff --git a/documentation/content/docs/protobuf.mdx b/documentation/content/docs/protobuf.mdx new file mode 100644 index 0000000..acf64a9 --- /dev/null +++ b/documentation/content/docs/protobuf.mdx @@ -0,0 +1,148 @@ +--- +title: Protocol Buffers +description: Enable and integrate optional protobuf payload inspection +--- + +## Activation + +Protocol Buffers support is available in Dari 1.6.0 and later. There is no feature flag and no `DariConfig` toggle for it. + +The integration is opt-in at the interceptor boundary: + +1. Create the interceptor with a `ProtobufPayloadDecoder`. +2. Pass the original `ByteArray` to the protobuf request and response methods. + +The same interceptor still supports every existing String/JSON method because `ProtobufDariInterceptor` extends `DariInterceptor`. + +| Requirement | Needed? | Notes | +| --- | --- | --- | +| Dari 1.6.0 or later | Yes | Use matching `dari` and `dari-noop` versions | +| `protobufEnabled` flag | No | No protobuf configuration flag exists | +| App-owned protobuf runtime and generated messages | Yes | Dari does not add or select a protobuf runtime | +| Decoder that returns display text | Yes | It may return `null` when a handler is unknown | +| Base64 | Only for string-only bridges | Dari itself accepts the decoded `ByteArray` | +| Changes to `.proto` files or message fields | No | Existing schemas and wire payloads remain unchanged | + +## 1. Create a protobuf-capable interceptor + +The application owns schema-aware decoding. Return a human-readable string for each handler and request/response type; JSON is recommended when convenient because Dari formats it in the decoded view. + +```kotlin +import com.easyhooon.dari.Dari +import com.easyhooon.dari.interceptor.PayloadPart.REQUEST +import com.easyhooon.dari.interceptor.PayloadPart.RESPONSE +import com.easyhooon.dari.interceptor.ProtobufDariInterceptor +import com.easyhooon.dari.interceptor.ProtobufPayloadDecoder + +val interceptor: ProtobufDariInterceptor? = Dari.createInterceptor( + tag = "OrderBridge", + protobufDecoder = ProtobufPayloadDecoder { payload, context -> + when (context.handlerName to context.part) { + "createOrder" to REQUEST -> + JsonFormat.printer().print(CreateOrderRequest.parseFrom(payload)) + "createOrder" to RESPONSE -> + JsonFormat.printer().print(CreateOrderResponse.parseFrom(payload)) + else -> null + } + }, +) +``` + +If only raw inspection is needed, the decoder can intentionally return `null`: + +```kotlin +val interceptor = Dari.createInterceptor( + protobufDecoder = ProtobufPayloadDecoder { _, _ -> null }, +) +``` + +Dari will record the payload as protobuf with `DECODER_UNAVAILABLE` while keeping its raw preview and original byte size. + +## 2. Capture the original bytes + +Call Dari at the same request and response boundaries already used by the bridge. Do not convert the payload to JSON before capture. + +```kotlin +interceptor?.onWebToAppProtobufRequest( + handlerName = "createOrder", + requestId = requestId, + requestData = requestBytes, +) + +val response = CreateOrderResponse.newBuilder() + .setOrderId("order-123") + .build() + +interceptor?.onWebToAppProtobufResponse( + handlerName = "createOrder", + requestId = requestId, + responseData = response.toByteArray(), + isSuccess = true, +) +``` + +Use a stable `requestId` to pair a request and response. Pass `null` only for standalone or fire-and-forget messages. + +## Direction and method mapping + +| Bridge event | Dari method | +| --- | --- | +| Web sends request to App | `onWebToAppProtobufRequest()` | +| App responds to Web | `onWebToAppProtobufResponse()` | +| App sends request to Web | `onAppToWebProtobufRequest()` | +| Web responds to App | `onAppToWebProtobufResponse()` | + +`handlerName`, direction, and request/response part are provided to the decoder through `ProtobufDecodeContext`, allowing request and response messages to use different generated types. + +## Base64 and bridge transport + +Base64 is not required by protobuf or Dari. + +- If the bridge already provides bytes, pass them directly to Dari. +- If a string-only bridge transports Base64, decode it first and pass the resulting bytes. + +```kotlin +@JavascriptInterface +fun onProtobufRequest(requestId: String, base64Data: String) { + val bytes = Base64.decode(base64Data, Base64.NO_WRAP) + interceptor?.onWebToAppProtobufRequest( + handlerName = "createOrder", + requestId = requestId, + requestData = bytes, + ) +} +``` + +The Base64 shown in Dari's Raw view is generated for display and does not indicate how the application transported the payload. + +## What Dari displays + +The REQUEST and RESPONSE tabs provide two views: + +- **Decoded**: text returned by the application-provided decoder +- **Raw**: the captured protobuf bytes rendered as Hex and Base64 + +The `Decoded` and `Raw` controls are available independently in both the REQUEST and RESPONSE tabs. This lets you compare the schema-aware value with the actual wire bytes for each side of the bridge exchange. + + + + + + + + + + + + + + +
DecodedRaw (Hex and Base64)
Decoded protobuf response in DariRaw protobuf response as Hex and Base64 in Dari
+ +The screenshots show the RESPONSE tab; the REQUEST tab provides the same two views. Dari records the original byte size and decode status. The raw capture is limited to the first 4 KB, and larger payloads are marked as truncated. Decoder failures are recorded without interrupting bridge communication. + +## Runtime behavior + +- Decoding runs synchronously on the interceptor caller's thread, so keep parsing and display conversion bounded. +- Release builds use `dari-noop`; `Dari.createInterceptor()` returns `null`, and safe calls have no runtime overhead. +- Existing String/JSON calls can continue on the same protobuf-capable interceptor. diff --git a/documentation/public/protobuf-decoded.png b/documentation/public/protobuf-decoded.png new file mode 100644 index 0000000..99ac345 Binary files /dev/null and b/documentation/public/protobuf-decoded.png differ diff --git a/documentation/public/protobuf-raw.png b/documentation/public/protobuf-raw.png new file mode 100644 index 0000000..8aa8450 Binary files /dev/null and b/documentation/public/protobuf-raw.png differ