This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Kotlin/JVM client library for the ODXProxy Gateway — a proxy that fronts Odoo instances. Published to Maven Central as io.odxproxy:odxproxyclient-java. Written in Kotlin but compiled to Java 8 bytecode (jvmTarget = "1.8", sourceCompatibility = 1.8) for Java 8+ consumer interop (Android, JavaFX, Spring Boot). Toolchain JDK 17.
./gradlew build # compile + test
./gradlew test # run all JUnit 5 tests
./gradlew test --tests "io.odxproxy.OdxProxyMockTest" # single class
./gradlew test --tests "io.odxproxy.OdxProxyMockTest.someName" # single method
./gradlew publishToMavenLocal # local publishTests live under src/main/test/kotlin (note: under main/, not the conventional src/test/). OdxProxyMockTest uses OkHttp MockWebServer; OdxProxyLiveTest hits a real gateway.
Live tests run automatically when an untracked odx-test.properties exists, and OdxProxyLiveTest writes real records. For offline checks, run only the mock classes:
./gradlew test --tests "io.odxproxy.OdxProxyMockTest" --tests "io.odxproxy.OdxProxyV2MockTest"OdxProxyV2MockTest asserts the exact /v2/odoo/execute body per OdxProxyV2 method and the exception helpers. OdxProxyV2LiveTest is gated by env vars, not the properties file: ODX_V2_GATEWAY_URL, ODX_V2_API_KEY, ODX_V2_ODOO_URL, ODX_V2_ODOO_DB, ODX_V2_ODOO_API_KEY. Its write lifecycle also needs ODX_V2_ALLOW_WRITES=1. It needs ODXProxy 0.9.0+ and Odoo 19+.
A pom.xml exists alongside build.gradle.kts — Gradle is the source of truth. The Maven file mirrors it for IDEs/consumers that prefer Maven metadata.
Three layers, all in src/main/kotlin/io/odxproxy/:
-
OdxProxy(singleton facade) —OdxProxy.kt. Static@JvmStaticentrypoints (search,searchRead,read,searchCount,create,write,remove,fieldsGet,callMethod). Each method builds anOdxClientRequestwith a stringaction("search_read", "unlink", "call_method", …) and delegates to the client. Request IDs auto-generated as ULIDs when caller passesnull. -
OdxProxyClient(transport) —client/OdxProxyClient.kt. Process-wide singleton guarded byAtomicReference;init()throws if called twice,getInstance()throws if not initialized. Holds the OkHttp client (10s connect, 45s read/write) and a singleJsonconfigured withignoreUnknownKeys = true,explicitNulls = false,isLenient = true. All requests POST to${gatewayUrl}/api/odoo/executewithX-Api-Keyheader. Both serialization and network I/O run off-caller-thread viaCompletableFuture.runAsync— the public contract is that nothing blocks the caller (UI-thread safe). Result type is resolved at runtime viaserializersModule.serializer(Class<T>), soTmust be@Serializable(or a built-in kotlinx type likeJsonObject).
2b. OdxProxyV2 (v2 facade): OdxProxyV2.kt, for ODXProxy 0.9.0+ /v2/odoo/* (Odoo JSON-2, Odoo 19+). It uses the same singleton client and instance as OdxProxy.
- Methods mirror v1's names (
search,searchRead,read,searchCount,create,createOne,write,remove,fieldsGet,callMethod,version,isSupported), with@JvmStatic @JvmOverloads. - Each builds
kwargskeyed by Odoo's Python parameter names verbatim viabuildKwargs, which drops nulls and mergesOdxProxyClientInfo.defaultContext<kwargs["context"]< the call'scontext. It then posts anOdxV2RequestthroughOdxProxyClient.postV2Request/postV2RequestList. odoo_instanceis sent withoutuser_id.createalways sendsvals_listand returnsList<Int>;createOneunwraps it.removesendsunlink.isSupportedcaches per URL in aConcurrentHashMap.OdxProxyClient.executeCallis shared by both versions: endpoint and body serializer are parameters.
- Model layer —
model/:Models.kt: request/response envelopes (OdxClientRequest,OdxServerResponse<T>,OdxClientKeywordRequest,OdxInstanceInfo).OdxIdSerializeraccepts JSON id as either string or number and normalizes toString.toJsonElement(Any?)recursively converts arbitrary Java/Kotlin containers (List,Map,Array, primitives) intoJsonElement— this is how Java callers can pass plainMap/Listasparamswithout writing serializers.OdooTypes.kt: the core defense against Odoo's polymorphic JSON.OdxMany2Onedecodesfalse | null | [id, "Name"] | []into a uniform(id, name)pair.OdxVariant<T>wraps fields that Odoo may return asfalseinstead of the declared type — when the value is the literal booleanfalseit decodes toOdxVariant(null)rather than throwing. When mapping Odoo records to POJOs, always use these wrappers for relational / nullable string fields. Do not use rawString?/Int?for fields Odoo can return asfalse.
exception/Exceptions.kt defines OdxServerErrorException, with constructors for parsed JSON-RPC error envelopes and for raw HTTP failures.
- Every throw site in
OdxProxyClientpasses the response'shttpStatus. - Helpers:
odooStatus(Odoo's HTTP status, forwarded ascodeon a 200),odooErrorName,isLicenseError(code 0 on HTTP 403, because Odoo 19+/jsonrpcerrors are also code 0 but on HTTP 200),isJson2Unavailable(-32006),isInvalidRequest(-32007),isRetryable. - Code constants live on the companion object (e.g.
OdxServerErrorException.JSON2_UNAVAILABLE). - There is deliberately one exception class, not a hierarchy.
kotlin { explicitApi() } is on — every public declaration needs explicit public and an explicit return type.
OdxClientKeywordRequest.resetPagination() zeroes fields/order/limit/offset and is called inside OdxProxy for actions where pagination is meaningless (search, read, create, write, unlink, fields_get). search_read, search_count, and call_method pass the keyword through unchanged. Preserve this distinction when adding new actions.
The README's "What an LLM reading consumer code should enforce" section is the consumer-facing AI guide (previously a separate AI_SDK_GUIDE.md, now folded in). When you change the public surface — new wrapper types, new anti-patterns, new actions — update that table.