From 0944d5a3d6224a80ea86ac9946d93b2cc65b16a2 Mon Sep 17 00:00:00 2001 From: ivanauth Date: Thu, 15 Jan 2026 11:39:12 -0500 Subject: [PATCH] docs: Add Kotlin and Scala examples for JVM language support Closes #11 Added example code demonstrating how to use the Authzed Java client library from Kotlin and Scala, making it easier for developers in those ecosystems to integrate SpiceDB. The examples show idiomatic usage patterns for each language while using the same underlying Java client library. Changes: - Add Kotlin example (examples/kotlin/CallingCheck.kt) with concise syntax and better null safety - Add Scala example (examples/scala/CallingCheck.scala) with functional programming patterns using Try and Option monads - Add comprehensive README (examples/README.md) documenting installation and usage for Java, Kotlin, and Scala with build tool configurations - Fix unused import in Scala example - Make dependency instructions consistent for grpc-netty-shaded across all build tools - Add Eclipse IDE files to .gitignore Signed-off-by: ivanauth --- .gitignore | 4 + examples/README.md | 130 ++++++++++++++++++++ examples/kotlin/CallingCheck.kt | 188 +++++++++++++++++++++++++++++ examples/scala/CallingCheck.scala | 193 ++++++++++++++++++++++++++++++ 4 files changed, 515 insertions(+) create mode 100644 examples/README.md create mode 100644 examples/kotlin/CallingCheck.kt create mode 100644 examples/scala/CallingCheck.scala diff --git a/.gitignore b/.gitignore index 9e553e6f..180370b3 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,7 @@ bin gradle/wrapper !gradle/wrapper/gradle-wrapper.jar !gradle/wrapper/gradle-wrapper.properties + +# Eclipse/Buildship +.project +.settings/ diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..6d2aae2f --- /dev/null +++ b/examples/README.md @@ -0,0 +1,130 @@ +# Authzed Java Client Examples + +This directory contains examples demonstrating how to use the Authzed Java client library from various JVM languages. + +## Available Examples + +### Java Examples (`v1/`) +- `CallingCheck.java` - Demonstrates basic permission checking with schema writing, relationship creation, and permission checks +- `CallingWatch.java` - Demonstrates watching for changes in SpiceDB with automatic reconnection + +### Kotlin Examples (`kotlin/`) +- `CallingCheck.kt` - Same functionality as the Java example, but using idiomatic Kotlin syntax + +### Scala Examples (`scala/`) +- `CallingCheck.scala` - Same functionality as the Java example, but using idiomatic Scala syntax + +## Installation + +The Authzed Java client library works seamlessly with all JVM languages. Choose your build tool and language: + +**Note:** You need to include a gRPC transport implementation like `grpc-netty-shaded` to run these examples. The `authzed` library doesn't include a transport by default to give you flexibility in choosing one. + +### Maven (Java) + +```xml + + + com.authzed.api + authzed + 1.5.4 + + + io.grpc + grpc-api + 1.75.0 + + + io.grpc + grpc-stub + 1.75.0 + + + io.grpc + grpc-netty-shaded + 1.75.0 + + +``` + +### Gradle (Java/Kotlin) + +For `build.gradle`: +```groovy +dependencies { + implementation "com.authzed.api:authzed:1.5.4" + implementation 'io.grpc:grpc-api:1.75.0' + implementation 'io.grpc:grpc-stub:1.75.0' + implementation 'io.grpc:grpc-netty-shaded:1.75.0' +} +``` + +For `build.gradle.kts` (Kotlin DSL): +```kotlin +dependencies { + implementation("com.authzed.api:authzed:1.5.4") + implementation("io.grpc:grpc-api:1.75.0") + implementation("io.grpc:grpc-stub:1.75.0") + implementation("io.grpc:grpc-netty-shaded:1.75.0") +} +``` + +### SBT (Scala) + +```scala +libraryDependencies ++= Seq( + "com.authzed.api" % "authzed" % "1.5.4", + "io.grpc" % "grpc-api" % "1.75.0", + "io.grpc" % "grpc-stub" % "1.75.0", + "io.grpc" % "grpc-netty-shaded" % "1.75.0" +) +``` + +## Usage + +All examples follow the same pattern: + +1. **Create a channel** - Connect to SpiceDB using gRPC +2. **Authenticate** - Use a bearer token for authentication +3. **Create service stubs** - Initialize the schema and permissions services +4. **Write a schema** - Define your permission model +5. **Write relationships** - Create relationships between objects +6. **Check permissions** - Verify if a subject has permission on a resource + +### Key Differences Between Languages + +#### Java +- Uses verbose builder pattern +- Requires explicit exception handling with try-catch +- Traditional class structure + +#### Kotlin +- More concise syntax with named parameters and string interpolation +- Better null safety with nullable types (`?`) +- Can use `try-catch` as expressions +- Cleaner lambda syntax + +#### Scala +- Functional programming style with `Try` and `Option` monads +- Pattern matching with `recover` +- For-comprehensions for sequential operations (not shown in basic example) +- Immutable by default + +## Running the Examples + +These examples are meant to be copied into your own project. They demonstrate the API usage patterns but are not standalone runnable programs without: + +1. A valid SpiceDB instance (local or cloud) +2. A valid authentication token +3. Proper build configuration for your language + +## Getting Started + +For a complete guide on integrating SpiceDB with your application, see the [Protecting Your First App](https://authzed.com/docs/guides/first-app) guide. + +## Additional Resources + +- [Authzed Java Client Documentation](https://authzed.github.io/authzed-java/index.html) +- [SpiceDB API Reference](https://authzed.com/docs/spicedb/api/http-api) +- [Buf Registry SpiceDB API](https://buf.build/authzed/api/docs/main) +- [SpiceDB Documentation](https://authzed.com/docs) diff --git a/examples/kotlin/CallingCheck.kt b/examples/kotlin/CallingCheck.kt new file mode 100644 index 00000000..480df07b --- /dev/null +++ b/examples/kotlin/CallingCheck.kt @@ -0,0 +1,188 @@ +/* + * Authzed API examples - Kotlin + * + * This example demonstrates how to use the Authzed Java client library from Kotlin. + * It shows the more idiomatic Kotlin syntax while using the same Java client library. + */ + +import com.authzed.api.v1.* +import com.authzed.grpcutil.BearerToken +import io.grpc.ManagedChannel +import io.grpc.ManagedChannelBuilder +import java.util.concurrent.TimeUnit +import java.util.logging.Level +import java.util.logging.Logger + +// Installation +// Add to your build.gradle.kts: +// dependencies { +// implementation("com.authzed.api:authzed:1.5.4") +// implementation("io.grpc:grpc-api:1.75.0") +// implementation("io.grpc:grpc-stub:1.75.0") +// implementation("io.grpc:grpc-netty-shaded:1.75.0") +// } + +class AuthzedClient(channel: ManagedChannel, token: String) { + private val logger = Logger.getLogger(AuthzedClient::class.java.name) + private val bearerToken = BearerToken(token) + + private val schemaService: SchemaServiceGrpc.SchemaServiceBlockingStub = + SchemaServiceGrpc.newBlockingStub(channel) + .withCallCredentials(bearerToken) + + private val permissionsService: PermissionsServiceGrpc.PermissionsServiceBlockingStub = + PermissionsServiceGrpc.newBlockingStub(channel) + .withCallCredentials(bearerToken) + + fun writeSchema(): String { + logger.info("Writing schema...") + + val schema = """ + definition thelargeapp/article { + relation author: thelargeapp/user + relation commenter: thelargeapp/user + + permission can_comment = commenter + author + } + + definition thelargeapp/user {} + """.trimIndent() + + val request = WriteSchemaRequest.newBuilder() + .setSchema(schema) + .build() + + return try { + val response = schemaService.writeSchema(request) + logger.info("Response: $response") + response.toString() + } catch (e: Exception) { + logger.log(Level.WARNING, "RPC failed: ${e.message}") + "" + } + } + + fun readSchema(): String { + logger.info("Reading schema...") + + val request = ReadSchemaRequest.newBuilder().build() + + return try { + val response = schemaService.readSchema(request) + logger.info(response.toString()) + response.toString() + } catch (e: Exception) { + logger.log(Level.WARNING, "RPC failed: ${e.message}") + "" + } + } + + fun writeRelationship(): String { + logger.info("Write relationship...") + + val request = WriteRelationshipsRequest.newBuilder() + .addUpdates( + RelationshipUpdate.newBuilder() + .setOperation(RelationshipUpdate.Operation.OPERATION_CREATE) + .setRelationship( + Relationship.newBuilder() + .setResource( + ObjectReference.newBuilder() + .setObjectType("thelargeapp/article") + .setObjectId("kotlin_test") + .build() + ) + .setRelation("author") + .setSubject( + SubjectReference.newBuilder() + .setObject( + ObjectReference.newBuilder() + .setObjectType("thelargeapp/user") + .setObjectId("george") + .build() + ) + .build() + ) + .build() + ) + .build() + ) + .build() + + return try { + val response = permissionsService.writeRelationships(request) + logger.info("Response: $response") + response.writtenAt.token + } catch (e: Exception) { + logger.log(Level.WARNING, "RPC failed: ${e.message}") + "" + } + } + + fun check(zedToken: ZedToken): CheckPermissionResponse.Permissionship? { + logger.info("Checking...") + + val request = CheckPermissionRequest.newBuilder() + .setConsistency( + Consistency.newBuilder() + .setAtLeastAsFresh(zedToken) + .build() + ) + .setResource( + ObjectReference.newBuilder() + .setObjectType("thelargeapp/article") + .setObjectId("kotlin_test") + .build() + ) + .setSubject( + SubjectReference.newBuilder() + .setObject( + ObjectReference.newBuilder() + .setObjectType("thelargeapp/user") + .setObjectId("george") + .build() + ) + .build() + ) + .setPermission("can_comment") + .build() + + return try { + val response = permissionsService.checkPermission(request) + logger.info("Response: $response") + response.permissionship + } catch (e: Exception) { + logger.log(Level.WARNING, "RPC failed: ${e.message}") + null + } + } +} + +fun main() { + val target = "grpc.authzed.com:443" + val token = "tc_test_def_token" + + val channel = ManagedChannelBuilder + .forTarget(target) + .useTransportSecurity() // for local development without TLS, use .usePlaintext() + .build() + + try { + val client = AuthzedClient(channel, token) + + client.writeSchema() + client.readSchema() + + val tokenVal = client.writeRelationship() + + val result = client.check( + ZedToken.newBuilder() + .setToken(tokenVal) + .build() + ) + + println("Check result: $result") + } finally { + channel.shutdownNow().awaitTermination(5, TimeUnit.SECONDS) + } +} diff --git a/examples/scala/CallingCheck.scala b/examples/scala/CallingCheck.scala new file mode 100644 index 00000000..36b67b41 --- /dev/null +++ b/examples/scala/CallingCheck.scala @@ -0,0 +1,193 @@ +/* + * Authzed API examples - Scala + * + * This example demonstrates how to use the Authzed Java client library from Scala. + * It shows the more idiomatic Scala syntax while using the same Java client library. + */ + +import com.authzed.api.v1._ +import com.authzed.grpcutil.BearerToken +import io.grpc.{ManagedChannel, ManagedChannelBuilder} +import java.util.concurrent.TimeUnit +import java.util.logging.{Level, Logger} +import scala.util.Try + +// Installation +// Add to your build.sbt: +// libraryDependencies ++= Seq( +// "com.authzed.api" % "authzed" % "1.5.4", +// "io.grpc" % "grpc-api" % "1.75.0", +// "io.grpc" % "grpc-stub" % "1.75.0", +// "io.grpc" % "grpc-netty-shaded" % "1.75.0" +// ) + +class AuthzedClient(channel: ManagedChannel, token: String) { + private val logger = Logger.getLogger(classOf[AuthzedClient].getName) + private val bearerToken = new BearerToken(token) + + private val schemaService: SchemaServiceGrpc.SchemaServiceBlockingStub = + SchemaServiceGrpc.newBlockingStub(channel) + .withCallCredentials(bearerToken) + + private val permissionsService: PermissionsServiceGrpc.PermissionsServiceBlockingStub = + PermissionsServiceGrpc.newBlockingStub(channel) + .withCallCredentials(bearerToken) + + def writeSchema(): String = { + logger.info("Writing schema...") + + val schema = + """definition thelargeapp/article { + | relation author: thelargeapp/user + | relation commenter: thelargeapp/user + | + | permission can_comment = commenter + author + |} + | + |definition thelargeapp/user {} + |""".stripMargin + + val request = WriteSchemaRequest.newBuilder() + .setSchema(schema) + .build() + + Try { + val response = schemaService.writeSchema(request) + logger.info(s"Response: $response") + response.toString + }.recover { + case e: Exception => + logger.log(Level.WARNING, s"RPC failed: ${e.getMessage}") + "" + }.get + } + + def readSchema(): String = { + logger.info("Reading schema...") + + val request = ReadSchemaRequest.newBuilder().build() + + Try { + val response = schemaService.readSchema(request) + logger.info(response.toString) + response.toString + }.recover { + case e: Exception => + logger.log(Level.WARNING, s"RPC failed: ${e.getMessage}") + "" + }.get + } + + def writeRelationship(): String = { + logger.info("Write relationship...") + + val request = WriteRelationshipsRequest.newBuilder() + .addUpdates( + RelationshipUpdate.newBuilder() + .setOperation(RelationshipUpdate.Operation.OPERATION_CREATE) + .setRelationship( + Relationship.newBuilder() + .setResource( + ObjectReference.newBuilder() + .setObjectType("thelargeapp/article") + .setObjectId("scala_test") + .build() + ) + .setRelation("author") + .setSubject( + SubjectReference.newBuilder() + .setObject( + ObjectReference.newBuilder() + .setObjectType("thelargeapp/user") + .setObjectId("george") + .build() + ) + .build() + ) + .build() + ) + .build() + ) + .build() + + Try { + val response = permissionsService.writeRelationships(request) + logger.info(s"Response: $response") + response.getWrittenAt.getToken + }.recover { + case e: Exception => + logger.log(Level.WARNING, s"RPC failed: ${e.getMessage}") + "" + }.get + } + + def check(zedToken: ZedToken): Option[CheckPermissionResponse.Permissionship] = { + logger.info("Checking...") + + val request = CheckPermissionRequest.newBuilder() + .setConsistency( + Consistency.newBuilder() + .setAtLeastAsFresh(zedToken) + .build() + ) + .setResource( + ObjectReference.newBuilder() + .setObjectType("thelargeapp/article") + .setObjectId("scala_test") + .build() + ) + .setSubject( + SubjectReference.newBuilder() + .setObject( + ObjectReference.newBuilder() + .setObjectType("thelargeapp/user") + .setObjectId("george") + .build() + ) + .build() + ) + .setPermission("can_comment") + .build() + + Try { + val response = permissionsService.checkPermission(request) + logger.info(s"Response: $response") + Some(response.getPermissionship) + }.recover { + case e: Exception => + logger.log(Level.WARNING, s"RPC failed: ${e.getMessage}") + None + }.get + } +} + +object CallingCheck { + def main(args: Array[String]): Unit = { + val target = "grpc.authzed.com:443" + val token = "tc_test_def_token" + + val channel = ManagedChannelBuilder + .forTarget(target) + .useTransportSecurity() // for local development without TLS, use .usePlaintext() + .build() + + try { + val client = new AuthzedClient(channel, token) + + client.writeSchema() + client.readSchema() + + val tokenVal = client.writeRelationship() + + val result = client.check( + ZedToken.newBuilder() + .setToken(tokenVal) + .build() + ) + + println(s"Check result: $result") + } finally { + channel.shutdownNow().awaitTermination(5, TimeUnit.SECONDS) + } + } +}