From 2f7a514b12fc4683956450f831fbee28b4553b70 Mon Sep 17 00:00:00 2001 From: Nishant Joshi Date: Thu, 11 Jul 2024 02:09:55 +0530 Subject: [PATCH 1/9] chore: add boilerplate with working cac client --- clients/pauli/.gitignore | 2 + clients/pauli/Makefile | 9 +++++ clients/pauli/build.zig | 71 ++++++++++++++++++++++++++++++++ clients/pauli/build.zig.zon | 72 +++++++++++++++++++++++++++++++++ clients/pauli/src/cac.zig | 80 +++++++++++++++++++++++++++++++++++++ clients/pauli/src/expr.zig | 0 clients/pauli/src/root.zig | 6 +++ 7 files changed, 240 insertions(+) create mode 100644 clients/pauli/.gitignore create mode 100644 clients/pauli/Makefile create mode 100644 clients/pauli/build.zig create mode 100644 clients/pauli/build.zig.zon create mode 100644 clients/pauli/src/cac.zig create mode 100644 clients/pauli/src/expr.zig create mode 100644 clients/pauli/src/root.zig diff --git a/clients/pauli/.gitignore b/clients/pauli/.gitignore new file mode 100644 index 00000000..308b00d2 --- /dev/null +++ b/clients/pauli/.gitignore @@ -0,0 +1,2 @@ +.zig-cache/* +zig-out/* diff --git a/clients/pauli/Makefile b/clients/pauli/Makefile new file mode 100644 index 00000000..d03781aa --- /dev/null +++ b/clients/pauli/Makefile @@ -0,0 +1,9 @@ + + +.PHONY: test coverage run + + +test: test-cac + +test-cac: + zig test -I../../headers -lc src/cac.zig ../../target/debug/libcac_client.so diff --git a/clients/pauli/build.zig b/clients/pauli/build.zig new file mode 100644 index 00000000..8f5400e6 --- /dev/null +++ b/clients/pauli/build.zig @@ -0,0 +1,71 @@ +const std = @import("std"); + +// Although this function looks imperative, note that its job is to +// declaratively construct a build graph that will be executed by an external +// runner. +pub fn build(b: *std.Build) void { + // Standard target options allows the person running `zig build` to choose + // what target to build for. Here we do not override the defaults, which + // means any target is allowed, and the default is native. Other options + // for restricting supported target set are available. + const target = b.standardTargetOptions(.{}); + + // Standard optimization options allow the person running `zig build` to select + // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not + // set a preferred release mode, allowing the user to decide how to optimize. + const optimize = b.standardOptimizeOption(.{}); + + const lib = b.addStaticLibrary(.{ + .name = "pauli", + // In this case the main source file is merely a path, however, in more + // complicated build scripts, this could be a generated file. + .root_source_file = b.path("src/root.zig"), + .target = target, + .optimize = optimize, + }); + + // Mention the include directory for the c_bindings header files + + linkBindings(b, lib); + + // This declares intent for the library to be installed into the standard + // location when the user invokes the "install" step (the default step when + // running `zig build`). + b.installArtifact(lib); + + // Tests section + + const test_step = b.step("test", "Run unit tests"); + + // Root Tests + + const lib_unit_tests = b.addTest(.{ + .root_source_file = b.path("src/root.zig"), + .target = target, + .optimize = optimize, + }); + + linkBindings(b, lib_unit_tests); + + const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); + test_step.dependOn(&run_lib_unit_tests.step); + + // CAC Tests + + const cac_unit_tests = b.addTest(.{ + .root_source_file = b.path("src/root.zig"), + .target = target, + .optimize = optimize, + }); + + linkBindings(b, lib_unit_tests); + + const run_cac_unit_tests = b.addRunArtifact(cac_unit_tests); + test_step.dependOn(&run_cac_unit_tests.step); +} + +fn linkBindings(b: *std.Build, c: *std.Build.Step.Compile) void { + c.linkLibC(); + c.addIncludePath(.{ .src_path = .{ .owner = b, .sub_path = "../../headers" } }); + c.addLibraryPath(.{ .src_path = .{ .owner = b, .sub_path = "../../target/debug" } }); +} diff --git a/clients/pauli/build.zig.zon b/clients/pauli/build.zig.zon new file mode 100644 index 00000000..6d4f73ec --- /dev/null +++ b/clients/pauli/build.zig.zon @@ -0,0 +1,72 @@ +.{ + // This is the default name used by packages depending on this one. For + // example, when a user runs `zig fetch --save `, this field is used + // as the key in the `dependencies` table. Although the user can choose a + // different name, most users will stick with this provided value. + // + // It is redundant to include "zig" in this name because it is already + // within the Zig package namespace. + .name = "pauli", + + // This is a [Semantic Version](https://semver.org/). + // In a future version of Zig it will be used for package deduplication. + .version = "0.0.0", + + // This field is optional. + // This is currently advisory only; Zig does not yet do anything + // with this value. + //.minimum_zig_version = "0.11.0", + + // This field is optional. + // Each dependency must either provide a `url` and `hash`, or a `path`. + // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. + // Once all dependencies are fetched, `zig build` no longer requires + // internet connectivity. + .dependencies = .{ + // See `zig fetch --save ` for a command-line interface for adding dependencies. + //.example = .{ + // // When updating this field to a new URL, be sure to delete the corresponding + // // `hash`, otherwise you are communicating that you expect to find the old hash at + // // the new URL. + // .url = "https://example.com/foo.tar.gz", + // + // // This is computed from the file contents of the directory of files that is + // // obtained after fetching `url` and applying the inclusion rules given by + // // `paths`. + // // + // // This field is the source of truth; packages do not come from a `url`; they + // // come from a `hash`. `url` is just one of many possible mirrors for how to + // // obtain a package matching this `hash`. + // // + // // Uses the [multihash](https://multiformats.io/multihash/) format. + // .hash = "...", + // + // // When this is provided, the package is found in a directory relative to the + // // build root. In this case the package's hash is irrelevant and therefore not + // // computed. This field and `url` are mutually exclusive. + // .path = "foo", + + // // When this is set to `true`, a package is declared to be lazily + // // fetched. This makes the dependency only get fetched if it is + // // actually used. + // .lazy = false, + //}, + }, + + // Specifies the set of files and directories that are included in this package. + // Only files and directories listed here are included in the `hash` that + // is computed for this package. Only files listed here will remain on disk + // when using the zig package manager. As a rule of thumb, one should list + // files required for compilation plus any license(s). + // Paths are relative to the build root. Use the empty string (`""`) to refer to + // the build root itself. + // A directory listed here means that all files within, recursively, are included. + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + // For example... + //"LICENSE", + //"README.md", + }, +} diff --git a/clients/pauli/src/cac.zig b/clients/pauli/src/cac.zig new file mode 100644 index 00000000..843e9168 --- /dev/null +++ b/clients/pauli/src/cac.zig @@ -0,0 +1,80 @@ +//! +//! Wrapper implementation around the CAC (Context Aware Config) client library. +//! + +// Imports +// --------------------------------------------------------------------------- + +const std = @import("std"); +const cac_bindings = @cImport({ + @cInclude("libcac_client.h"); +}); + +// Types +// --------------------------------------------------------------------------- + +const ConfigError = error{CStringConversionError}; +const ClientError = error{ClientNotInitialized}; + +const Config = struct { tenant: []const u8, update_frequency: u32, hostname: []const u8 }; + +const Client = struct { + /// + /// [`client`] is a pointer to the CAC client instance. + /// This is created when the cac client library is initialized. (see [`initCacClient`]) + /// + client: *cac_bindings.Arc_Client, + + /// + /// [`getClient`] returns a new client instance. + /// + /// Safety: This client as null pointer checks in place; if the client is not initialized the function returns an error. + /// + pub fn getClient(config: Config) ClientError!Client { + const unsafe_client = cac_bindings.cac_get_client(config.tenant.ptr); + + if (unsafe_client) |safe_client| { + return Client{ .client = safe_client }; + } else { + return ClientError.ClientNotInitialized; + } + } + + /// + /// [`freeClient`] frees the client instance. + /// + /// consider using this function to defer the cleanup of the client instance. + /// + /// + /// ```example + /// + /// const client = try Client.getClient(); + /// defer client.freeClient(); + /// + /// ``` + /// + pub fn freeClient(self: *Client) void { + cac_bindings.cac_free_client(self.client); + } +}; + +// Functions +// --------------------------------------------------------------------------- + +fn initCacClient(config: Config) ConfigError!void { + const output = cac_bindings.cac_new_client(config.tenant.ptr, config.update_frequency, config.hostname.ptr); + + std.debug.print("{}", .{output}); + + if (output == 1) { + return ConfigError.CStringConversionError; + } +} + +test "test initCacClient" { + const config = Config{ .tenant = "public", .update_frequency = 10, .hostname = "http://localhost:8081" }; + + try initCacClient(config); + var client = try Client.getClient(config); + defer client.freeClient(); +} diff --git a/clients/pauli/src/expr.zig b/clients/pauli/src/expr.zig new file mode 100644 index 00000000..e69de29b diff --git a/clients/pauli/src/root.zig b/clients/pauli/src/root.zig new file mode 100644 index 00000000..48b890f7 --- /dev/null +++ b/clients/pauli/src/root.zig @@ -0,0 +1,6 @@ +const std = @import("std"); +const testing = std.testing; + +export fn add(a: i32, b: i32) i32 { + return a + b; +} From 036f2bec54ea5aef0c2c67873a2d6edb7d356293 Mon Sep 17 00:00:00 2001 From: Nishant Joshi Date: Thu, 11 Jul 2024 11:18:54 +0530 Subject: [PATCH 2/9] chore: remove build step --- clients/pauli/Makefile | 2 +- clients/pauli/build.zig | 32 ++++++++++++++++++-------------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/clients/pauli/Makefile b/clients/pauli/Makefile index d03781aa..a087762c 100644 --- a/clients/pauli/Makefile +++ b/clients/pauli/Makefile @@ -6,4 +6,4 @@ test: test-cac test-cac: - zig test -I../../headers -lc src/cac.zig ../../target/debug/libcac_client.so + zig test -I../../headers -lc src/cac.zig ../../target/debug/libcac_client.so diff --git a/clients/pauli/build.zig b/clients/pauli/build.zig index 8f5400e6..b1cb3df0 100644 --- a/clients/pauli/build.zig +++ b/clients/pauli/build.zig @@ -4,6 +4,7 @@ const std = @import("std"); // declaratively construct a build graph that will be executed by an external // runner. pub fn build(b: *std.Build) void { + // Standard target options allows the person running `zig build` to choose // what target to build for. Here we do not override the defaults, which // means any target is allowed, and the default is native. Other options @@ -26,7 +27,7 @@ pub fn build(b: *std.Build) void { // Mention the include directory for the c_bindings header files - linkBindings(b, lib); + linkBindingsDebug(lib); // This declares intent for the library to be installed into the standard // location when the user invokes the "install" step (the default step when @@ -45,27 +46,30 @@ pub fn build(b: *std.Build) void { .optimize = optimize, }); - linkBindings(b, lib_unit_tests); + linkBindingsDebug(lib_unit_tests); const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); test_step.dependOn(&run_lib_unit_tests.step); - // CAC Tests + // // CAC Tests - const cac_unit_tests = b.addTest(.{ - .root_source_file = b.path("src/root.zig"), - .target = target, - .optimize = optimize, - }); + // const cac_unit_tests = b.addTest(.{ + // .name = "cac_unit_tests", + // .root_source_file = b.path("src/root.zig"), + // .target = target, + // .optimize = optimize, + // }); - linkBindings(b, lib_unit_tests); + // linkBindingsDebug(lib_unit_tests); - const run_cac_unit_tests = b.addRunArtifact(cac_unit_tests); - test_step.dependOn(&run_cac_unit_tests.step); + // const run_cac_unit_tests = b.addRunArtifact(cac_unit_tests); + // test_step.dependOn(&run_cac_unit_tests.step); } -fn linkBindings(b: *std.Build, c: *std.Build.Step.Compile) void { +fn linkBindingsDebug(c: *std.Build.Step.Compile) void { + c.addIncludePath(.{ .cwd_relative = "../../headers" }); + c.addLibraryPath(.{ .cwd_relative = "../../target/debug" }); + c.linkLibC(); - c.addIncludePath(.{ .src_path = .{ .owner = b, .sub_path = "../../headers" } }); - c.addLibraryPath(.{ .src_path = .{ .owner = b, .sub_path = "../../target/debug" } }); + c.linkSystemLibrary("cac_client"); } From 7914620bf9a7fdaa78af36541ab0ef2f6d5490ab Mon Sep 17 00:00:00 2001 From: Nishant Joshi Date: Thu, 11 Jul 2024 23:20:15 +0530 Subject: [PATCH 3/9] chore: add expt client --- clients/pauli/Makefile | 5 +- clients/pauli/build.zig | 14 ---- clients/pauli/src/cac.zig | 127 +++++++++++++++++++++++++------ clients/pauli/src/expr.zig | 0 clients/pauli/src/expt.zig | 151 +++++++++++++++++++++++++++++++++++++ clients/pauli/src/type.zig | 3 + 6 files changed, 262 insertions(+), 38 deletions(-) delete mode 100644 clients/pauli/src/expr.zig create mode 100644 clients/pauli/src/expt.zig create mode 100644 clients/pauli/src/type.zig diff --git a/clients/pauli/Makefile b/clients/pauli/Makefile index a087762c..57341523 100644 --- a/clients/pauli/Makefile +++ b/clients/pauli/Makefile @@ -3,7 +3,10 @@ .PHONY: test coverage run -test: test-cac +test: test-cac test-expt test-cac: zig test -I../../headers -lc src/cac.zig ../../target/debug/libcac_client.so + +test-expt: + zig test -I../../headers -lc src/expt.zig ../../target/debug/libexperimentation_client.so diff --git a/clients/pauli/build.zig b/clients/pauli/build.zig index b1cb3df0..cece6989 100644 --- a/clients/pauli/build.zig +++ b/clients/pauli/build.zig @@ -50,20 +50,6 @@ pub fn build(b: *std.Build) void { const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); test_step.dependOn(&run_lib_unit_tests.step); - - // // CAC Tests - - // const cac_unit_tests = b.addTest(.{ - // .name = "cac_unit_tests", - // .root_source_file = b.path("src/root.zig"), - // .target = target, - // .optimize = optimize, - // }); - - // linkBindingsDebug(lib_unit_tests); - - // const run_cac_unit_tests = b.addRunArtifact(cac_unit_tests); - // test_step.dependOn(&run_cac_unit_tests.step); } fn linkBindingsDebug(c: *std.Build.Step.Compile) void { diff --git a/clients/pauli/src/cac.zig b/clients/pauli/src/cac.zig index 843e9168..6cef3821 100644 --- a/clients/pauli/src/cac.zig +++ b/clients/pauli/src/cac.zig @@ -10,15 +10,14 @@ const cac_bindings = @cImport({ @cInclude("libcac_client.h"); }); +const ClientError = @import("type.zig").ClientError; +const SetupConfig = @import("type.zig").SetupConfig; + // Types // --------------------------------------------------------------------------- -const ConfigError = error{CStringConversionError}; -const ClientError = error{ClientNotInitialized}; - -const Config = struct { tenant: []const u8, update_frequency: u32, hostname: []const u8 }; - const Client = struct { + config: *const SetupConfig, /// /// [`client`] is a pointer to the CAC client instance. /// This is created when the cac client library is initialized. (see [`initCacClient`]) @@ -30,51 +29,133 @@ const Client = struct { /// /// Safety: This client as null pointer checks in place; if the client is not initialized the function returns an error. /// - pub fn getClient(config: Config) ClientError!Client { + fn getClient(config: SetupConfig) ClientError!Client { const unsafe_client = cac_bindings.cac_get_client(config.tenant.ptr); if (unsafe_client) |safe_client| { - return Client{ .client = safe_client }; + return Client{ .client = safe_client, .config = &config }; } else { return ClientError.ClientNotInitialized; } } /// - /// [`freeClient`] frees the client instance. + /// [`deinit`] frees the client instance. /// /// consider using this function to defer the cleanup of the client instance. /// /// /// ```example /// - /// const client = try Client.getClient(); - /// defer client.freeClient(); + /// const client = try Client.init(config); + /// defer client.deinit(); /// /// ``` /// - pub fn freeClient(self: *Client) void { + pub fn deinit(self: *const Client) void { cac_bindings.cac_free_client(self.client); } + + /// + /// [`init`] initializes the CAC client. + /// + /// This client is initialized outside the zig scope and is accessed via the [`getClient`] function. + /// This is done to manage a async runtime for managing cache and other artifacts optimizing the client. + /// + pub fn init(config: SetupConfig) ClientError!Client { + const output = cac_bindings.cac_new_client(config.tenant.ptr, config.update_frequency, config.hostname.ptr); + + if (output == 1) { + return ClientError.ClientInitializationError; + } + + return try Client.getClient(config); + } + + /// + /// [`poll`] starts polling the CAC client for updates. + /// + /// This function is used to start polling the CAC client for updates. + /// This is a blocking call, make sure to run this in a separate thread. The polling frequency is set in the [`StartupConfig`] struct using `update_frequency`. + /// + pub fn poll(self: *const Client) void { + cac_bindings.cac_start_polling_update(self.config.tenant.ptr); + } + + pub fn lastModifiedRaw(self: *const Client) ClientError![]const u8 { + const c_string = cac_bindings.cac_get_last_modified(self.client); + + const string: [:0]const u8 = std.mem.span(c_string); + + return string; + } + + fn getConfigRaw(self: *const Client, query: []const u8, prefix: []const u8) ClientError![]const u8 { + const c_string = cac_bindings.cac_get_config(self.client, query.ptr, prefix.ptr); + + if (c_string == null) { + return ClientError.FailedToGetConfig; + } + + const string: [:0]const u8 = std.mem.span(c_string); + + return string; + } + + fn getResolvedConfigRaw(self: *const Client, query: []const u8, filter_keys: []const u8, merge_strategy: []const u8) ClientError![]const u8 { + const c_string = cac_bindings.cac_get_resolved_config(self.client, query.ptr, filter_keys.ptr, merge_strategy.ptr); + + if (c_string == null) { + return ClientError.FailedToGetConfig; + } + + const string: [:0]const u8 = std.mem.span(c_string); + + return string; + } + + pub fn getDefaultConfigRaw(self: *const Client, filter_keys: []const u8) ClientError![]const u8 { + const c_string = cac_bindings.cac_get_default_config(self.client, filter_keys.ptr); + + if (c_string == null) { + return ClientError.FailedToGetConfig; + } + + const string: [:0]const u8 = std.mem.span(c_string); + + return string; + } + + /// + /// + /// [`freeFfiStringRaw`] frees the memory allocated by the CAC client. + /// + /// As the memory allocations are done by the CAC client, it is important to free the memory allocated by the client. + /// + /// Used to construct safe abstractions around the raw functions. (This can be used to copy over data to a memory space allocated by the provided allocator) + /// + fn freeFfiStringRaw(s_ptr: []const u8) void { + cac_bindings.cac_free_string(s_ptr.ptr); + } }; // Functions // --------------------------------------------------------------------------- -fn initCacClient(config: Config) ConfigError!void { - const output = cac_bindings.cac_new_client(config.tenant.ptr, config.update_frequency, config.hostname.ptr); - - std.debug.print("{}", .{output}); +test "test init" { + const config = SetupConfig{ .tenant = "public", .update_frequency = 10, .hostname = "http://localhost:8081" }; - if (output == 1) { - return ConfigError.CStringConversionError; - } + const client = try Client.init(config); + defer client.deinit(); } -test "test initCacClient" { - const config = Config{ .tenant = "public", .update_frequency = 10, .hostname = "http://localhost:8081" }; +test "test lastModifiedRaw" { + const config = SetupConfig{ .tenant = "public", .update_frequency = 10, .hostname = "http://localhost:8081" }; + + const client = try Client.init(config); + defer client.deinit(); + + const last_modified = try client.lastModifiedRaw(); - try initCacClient(config); - var client = try Client.getClient(config); - defer client.freeClient(); + try std.testing.expectEqualStrings(last_modified, "1970-01-01 00:00:00 UTC"); } diff --git a/clients/pauli/src/expr.zig b/clients/pauli/src/expr.zig deleted file mode 100644 index e69de29b..00000000 diff --git a/clients/pauli/src/expt.zig b/clients/pauli/src/expt.zig new file mode 100644 index 00000000..e2f910ce --- /dev/null +++ b/clients/pauli/src/expt.zig @@ -0,0 +1,151 @@ +//! +//! Wrapper implementation around the **Experimentation** client library. +//! + +// Imports +// --------------------------------------------------------------------------- + +const std = @import("std"); +const expt_bindings = @cImport({ + @cInclude("libexperimentation_client.h"); +}); + +const ClientError = @import("type.zig").ClientError; +const SetupConfig = @import("type.zig").SetupConfig; + +// Types +// --------------------------------------------------------------------------- + +const Client = struct { + config: *const SetupConfig, + + /// + /// [`client`] is a pointer to the underlying Experimentation client object. + /// + client: *expt_bindings.Arc_Client, + + /// + /// [`init`] initializes the Expt client. + /// + /// This client is initialized outside the zig scope and is accessed via the [`getClient`] function. + /// This is done to manage a async runtime for managing cache and other artifacts optimizing the client. + /// + pub fn init(config: SetupConfig) ClientError!Client { + const unsafe_client = expt_bindings.expt_new_client(config.tenant.ptr, config.update_frequency, config.hostname.ptr); + + if (unsafe_client == 1) { + return ClientError.ClientInitializationError; + } + + return try Client.getClient(config); + } + + /// + /// [`deinit`] frees the client instance. + /// + /// consider using this function to defer the cleanup of the client instance. + /// + /// + /// ```example + /// + /// const client = try Client.init(config); + /// defer client.deinit(); + /// + /// ``` + /// + pub fn deinit(self: *const Client) void { + expt_bindings.expt_free_client(self.client); + } + + /// + /// [`getClient`] returns a new client instance. + /// + /// Safety: This client as null pointer checks in place; if the client is not initialized the function returns an error. + /// + pub fn getClient(config: SetupConfig) ClientError!Client { + const unsafe_client = expt_bindings.expt_get_client(config.tenant.ptr); + + if (unsafe_client) |safe_client| { + return Client{ .config = &config, .client = safe_client }; + } else { + return ClientError.ClientNotInitialized; + } + } + + /// + /// [`poll`] starts polling the Expt client for updates. + /// + /// This function is used to start polling the Expt client for updates. + /// This is a blocking call, make sure to run this in a separate thread. The polling frequency is set in the [`StartupConfig`] struct using `update_frequency`. + /// + pub fn poll(self: *const Client) void { + expt_bindings.expt_start_polling_update(self.config.tenant.ptr); + } + + /// + /// + /// [`freeFfiStringRaw`] frees the memory allocated by the Expt client. + /// + /// As the memory allocations are done by the Expt client, it is important to free the memory allocated by the client. + /// + /// Used to construct safe abstractions around the raw functions. (This can be used to copy over data to a memory space allocated by the provided allocator) + /// + fn freeFfiStringRaw(s_ptr: []const u8) void { + expt_bindings.cac_free_string(s_ptr.ptr); + } + + fn getApplicableVariantRaw(self: *const Client, context: []const u8, toss: i16) ClientError![]const u8 { + const c_expr = expt_bindings.expt_get_applicable_variant(self.client, context.ptr, toss); + + if (c_expr == null) { + return ClientError.FailedToGetExperimentVariant; + } + + const expr: [:0]const u8 = std.mem.span(c_expr); + + return expr; + } + + fn getSatisfiedExperimentsRaw(self: *const Client, context: []const u8, filter_prefix: []const u8) ClientError![]const u8 { + const c_expr = expt_bindings.expt_get_satisfied_experiments(self.client, context.ptr, filter_prefix.ptr); + + if (c_expr == null) { + return ClientError.FailedToGetSatisfiedExperiments; + } + + const expr: [:0]const u8 = std.mem.span(c_expr); + + return expr; + } + + fn getFilteredSatisfiedExperimentsRaw(self: *const Client, context: []const u8, filter_prefix: []const u8) ClientError![]const u8 { + const c_expr = expt_bindings.expt_get_filtered_satisfied_experiments(self.client, context.ptr, filter_prefix.ptr); + + if (c_expr == null) { + return ClientError.FailedToGetFilteredSatisfiedExperiments; + } + + const expr: [:0]const u8 = std.mem.span(c_expr); + + return expr; + } + + fn getRunningExperimentsRaw(self: *const Client) ClientError![]const u8 { + const c_expr = expt_bindings.expt_get_running_experiments(self.client); + + if (c_expr == null) { + return ClientError.FailedToGetRunningExperiments; + } + + const expr: [:0]const u8 = std.mem.span(c_expr); + + return expr; + } +}; + +test "test init" { + const config = SetupConfig{ .tenant = "public", .update_frequency = 10, .hostname = "http://localhost:8081" }; + + const client = try Client.init(config); + defer client.deinit(); +} diff --git a/clients/pauli/src/type.zig b/clients/pauli/src/type.zig new file mode 100644 index 00000000..e928ac0c --- /dev/null +++ b/clients/pauli/src/type.zig @@ -0,0 +1,3 @@ +pub const ClientError = error{ ClientNotInitialized, ClientInitializationError, FailedToGetConfig, FailedToGetExperimentVariant }; + +pub const SetupConfig = struct { tenant: []const u8, update_frequency: u32, hostname: []const u8 }; From 8a9e6093b10447935bc1d8f8eddcac312fa9005d Mon Sep 17 00:00:00 2001 From: Nishant Joshi Date: Sun, 14 Jul 2024 11:17:19 +0530 Subject: [PATCH 4/9] chore: type abstractions added --- clients/pauli/Makefile | 10 +++- clients/pauli/README.md | 4 ++ clients/pauli/build.zig | 62 ++++++++++++++++------- clients/pauli/build.zig.zon | 2 +- clients/pauli/src/cac.zig | 80 +++++++++++++++++++++++------ clients/pauli/src/example_cli.zig | 20 ++++++++ clients/pauli/src/expt.zig | 83 +++++++++++++++++++++++++------ clients/pauli/src/root.zig | 8 +-- clients/pauli/src/utils.zig | 26 ++++++++++ 9 files changed, 238 insertions(+), 57 deletions(-) create mode 100644 clients/pauli/README.md create mode 100644 clients/pauli/src/example_cli.zig create mode 100644 clients/pauli/src/utils.zig diff --git a/clients/pauli/Makefile b/clients/pauli/Makefile index 57341523..44e5e987 100644 --- a/clients/pauli/Makefile +++ b/clients/pauli/Makefile @@ -1,6 +1,6 @@ -.PHONY: test coverage run +.PHONY: test coverage run run-ex build-ex test: test-cac test-expt @@ -10,3 +10,11 @@ test-cac: test-expt: zig test -I../../headers -lc src/expt.zig ../../target/debug/libexperimentation_client.so + + +run-ex: build-ex + ./zig-out/bin/cli + +build-ex: + zig build + diff --git a/clients/pauli/README.md b/clients/pauli/README.md new file mode 100644 index 00000000..cd14aa09 --- /dev/null +++ b/clients/pauli/README.md @@ -0,0 +1,4 @@ +### Pauli +A zig based client SDK for [`superposition`](https://github.com/juspay/superposition) service. + + diff --git a/clients/pauli/build.zig b/clients/pauli/build.zig index cece6989..05f9a8d3 100644 --- a/clients/pauli/build.zig +++ b/clients/pauli/build.zig @@ -4,34 +4,23 @@ const std = @import("std"); // declaratively construct a build graph that will be executed by an external // runner. pub fn build(b: *std.Build) void { - - // Standard target options allows the person running `zig build` to choose - // what target to build for. Here we do not override the defaults, which - // means any target is allowed, and the default is native. Other options - // for restricting supported target set are available. const target = b.standardTargetOptions(.{}); - // Standard optimization options allow the person running `zig build` to select - // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not - // set a preferred release mode, allowing the user to decide how to optimize. const optimize = b.standardOptimizeOption(.{}); + const superposition = buildSuperpositionClient(b); + const lib = b.addStaticLibrary(.{ .name = "pauli", - // In this case the main source file is merely a path, however, in more - // complicated build scripts, this could be a generated file. .root_source_file = b.path("src/root.zig"), .target = target, .optimize = optimize, }); - // Mention the include directory for the c_bindings header files + lib.step.dependOn(&superposition.step); - linkBindingsDebug(lib); + linkBindingsDebug(b, lib); - // This declares intent for the library to be installed into the standard - // location when the user invokes the "install" step (the default step when - // running `zig build`). b.installArtifact(lib); // Tests section @@ -46,16 +35,51 @@ pub fn build(b: *std.Build) void { .optimize = optimize, }); - linkBindingsDebug(lib_unit_tests); + linkBindingsDebug(b, lib_unit_tests); const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); test_step.dependOn(&run_lib_unit_tests.step); + + const example_1 = b.addExecutable(.{ .name = "cli", .root_source_file = b.path("src/example_cli.zig"), .target = target, .optimize = optimize }); + + linkBindingsDebug(b, example_1); + + b.installArtifact(example_1); + + const module = b.addModule("pauli", .{ + .root_source_file = b.path("src/root.zig"), + .target = target, + .optimize = optimize, + }); + + module.addIncludePath(b.path("../../headers")); + module.addLibraryPath(b.path("../../target/debug")); + + module.linkSystemLibrary("c", .{}); + + module.linkSystemLibrary("cac_client", .{}); + module.linkSystemLibrary("experimentation_client", .{}); } -fn linkBindingsDebug(c: *std.Build.Step.Compile) void { - c.addIncludePath(.{ .cwd_relative = "../../headers" }); - c.addLibraryPath(.{ .cwd_relative = "../../target/debug" }); +fn linkBindingsDebug(b: *std.Build, c: *std.Build.Step.Compile) void { + c.addIncludePath(b.path("../../headers")); + c.addLibraryPath(b.path("../../target/debug")); c.linkLibC(); c.linkSystemLibrary("cac_client"); + c.linkSystemLibrary("experimentation_client"); +} + +fn buildSuperpositionClient(b: *std.Build) *std.Build.Step.Run { + const repository = b.addSystemCommand(&.{ "git", "clone", "https://github.com/juspay/superposition.git" }); + + const rust_build = b.addSystemCommand(&.{ "cargo", "build", "--release" }); + + rust_build.setCwd(b.path("superposition")); + + rust_build.addDirectoryArg(b.path("superposition")); + + rust_build.step.dependOn(&repository.step); + + return rust_build; } diff --git a/clients/pauli/build.zig.zon b/clients/pauli/build.zig.zon index 6d4f73ec..4c53da6b 100644 --- a/clients/pauli/build.zig.zon +++ b/clients/pauli/build.zig.zon @@ -67,6 +67,6 @@ "src", // For example... //"LICENSE", - //"README.md", + "README.md", }, } diff --git a/clients/pauli/src/cac.zig b/clients/pauli/src/cac.zig index 6cef3821..77362113 100644 --- a/clients/pauli/src/cac.zig +++ b/clients/pauli/src/cac.zig @@ -10,13 +10,21 @@ const cac_bindings = @cImport({ @cInclude("libcac_client.h"); }); +const parse_json = @import("utils.zig").parse_json; +const parse_filter_keys = @import("utils.zig").parse_filter_keys; + const ClientError = @import("type.zig").ClientError; -const SetupConfig = @import("type.zig").SetupConfig; +pub const SetupConfig = @import("type.zig").SetupConfig; + +pub const MergeStrategy = enum { + MERGE, + REPLACE, +}; // Types // --------------------------------------------------------------------------- -const Client = struct { +pub const Client = struct { config: *const SetupConfig, /// /// [`client`] is a pointer to the CAC client instance. @@ -114,7 +122,7 @@ const Client = struct { return string; } - pub fn getDefaultConfigRaw(self: *const Client, filter_keys: []const u8) ClientError![]const u8 { + fn getDefaultConfigRaw(self: *const Client, filter_keys: []const u8) ClientError![]const u8 { const c_string = cac_bindings.cac_get_default_config(self.client, filter_keys.ptr); if (c_string == null) { @@ -126,31 +134,73 @@ const Client = struct { return string; } - /// - /// - /// [`freeFfiStringRaw`] frees the memory allocated by the CAC client. - /// - /// As the memory allocations are done by the CAC client, it is important to free the memory allocated by the client. - /// - /// Used to construct safe abstractions around the raw functions. (This can be used to copy over data to a memory space allocated by the provided allocator) - /// - fn freeFfiStringRaw(s_ptr: []const u8) void { - cac_bindings.cac_free_string(s_ptr.ptr); + // Allocation Based Functions + + pub fn getDefaultConfig(self: *const Client, comptime T: type, alloc: std.mem.Allocator, keys: []const []const u8) !T { + const output = try self.getDefaultConfigRaw(try parse_filter_keys(alloc, keys, "|")); + + const config = try parse_json(T, alloc, output); + + freeFfiStringRaw(output); + + return config; + } + + pub fn getResolvedConfig(self: *const Client, comptime O: type, alloc: std.mem.Allocator, query: anytype, filter_keys: []const []const u8, merge_strategy: MergeStrategy) !O { + const query_ptr = try std.json.stringifyAlloc(alloc, query, .{}); + + const output = try self.getResolvedConfigRaw(query_ptr, try parse_filter_keys(alloc, filter_keys, "|"), merge_strategy); + + const config = try parse_json(O, alloc, output); + + freeFfiStringRaw(output); + + return config; + } + + pub fn getConfig(self: *const Client, comptime O: type, alloc: std.mem.Allocator, query: anytype, prefix: []const []const u8) !O { + const prefix_ptr = try parse_filter_keys(alloc, prefix, ","); + + const query_ptr = try std.json.stringifyAlloc(alloc, query, .{}); + + std.debug.print("Query: {s}\n", .{query_ptr}); + + const output = try self.getConfigRaw(query_ptr, prefix_ptr); + + const config = try parse_json(O, alloc, output); + + freeFfiStringRaw(output); + + return config; } }; +/// +/// +/// [`freeFfiStringRaw`] frees the memory allocated by the CAC client. +/// +/// As the memory allocations are done by the CAC client, it is important to free the memory allocated by the client. +/// +/// Used to construct safe abstractions around the raw functions. (This can be used to copy over data to a memory space allocated by the provided allocator) +/// +fn freeFfiStringRaw(_: []const u8) void { + + // BUG: The current implementation of `cac_free_string` accepts `char *` but a significant other functions defined return `const char *`. This makes it impossible to free the memory allocated by the CAC client. + // cac_bindings.cac_free_string(s_ptr.ptr); +} + // Functions // --------------------------------------------------------------------------- test "test init" { - const config = SetupConfig{ .tenant = "public", .update_frequency = 10, .hostname = "http://localhost:8081" }; + const config = SetupConfig{ .tenant = "public", .update_frequency = 10, .hostname = "http://localhost:8080" }; const client = try Client.init(config); defer client.deinit(); } test "test lastModifiedRaw" { - const config = SetupConfig{ .tenant = "public", .update_frequency = 10, .hostname = "http://localhost:8081" }; + const config = SetupConfig{ .tenant = "public", .update_frequency = 10, .hostname = "http://localhost:8080" }; const client = try Client.init(config); defer client.deinit(); diff --git a/clients/pauli/src/example_cli.zig b/clients/pauli/src/example_cli.zig new file mode 100644 index 00000000..f406331d --- /dev/null +++ b/clients/pauli/src/example_cli.zig @@ -0,0 +1,20 @@ +const cac_client = @import("cac.zig"); +const std = @import("std"); + +pub fn main() !void { + var logging_alloc = std.heap.loggingAllocator(std.heap.page_allocator); + const alloc = logging_alloc.allocator(); + + const setup_config = cac_client.SetupConfig{ .tenant = "public", .update_frequency = 10, .hostname = "http://localhost:8080" }; + + const client = try cac_client.Client.init(setup_config); + + std.log.info("Client Initialized", .{}); + + const input = [_][]const u8{"test"}; + + const config = struct { test_2: u8 }; + + const output_1 = try client.getDefaultConfig(config, alloc, &input); + std.log.info("Output: {}", .{output_1}); +} diff --git a/clients/pauli/src/expt.zig b/clients/pauli/src/expt.zig index e2f910ce..36b5fdce 100644 --- a/clients/pauli/src/expt.zig +++ b/clients/pauli/src/expt.zig @@ -10,13 +10,16 @@ const expt_bindings = @cImport({ @cInclude("libexperimentation_client.h"); }); +const parse_json = @import("utils.zig").parse_json; +const parse_filter_keys = @import("utils.zig").parse_filter_keys; + const ClientError = @import("type.zig").ClientError; -const SetupConfig = @import("type.zig").SetupConfig; +pub const SetupConfig = @import("type.zig").SetupConfig; // Types // --------------------------------------------------------------------------- -const Client = struct { +pub const Client = struct { config: *const SetupConfig, /// @@ -62,7 +65,7 @@ const Client = struct { /// /// Safety: This client as null pointer checks in place; if the client is not initialized the function returns an error. /// - pub fn getClient(config: SetupConfig) ClientError!Client { + fn getClient(config: SetupConfig) ClientError!Client { const unsafe_client = expt_bindings.expt_get_client(config.tenant.ptr); if (unsafe_client) |safe_client| { @@ -82,18 +85,6 @@ const Client = struct { expt_bindings.expt_start_polling_update(self.config.tenant.ptr); } - /// - /// - /// [`freeFfiStringRaw`] frees the memory allocated by the Expt client. - /// - /// As the memory allocations are done by the Expt client, it is important to free the memory allocated by the client. - /// - /// Used to construct safe abstractions around the raw functions. (This can be used to copy over data to a memory space allocated by the provided allocator) - /// - fn freeFfiStringRaw(s_ptr: []const u8) void { - expt_bindings.cac_free_string(s_ptr.ptr); - } - fn getApplicableVariantRaw(self: *const Client, context: []const u8, toss: i16) ClientError![]const u8 { const c_expr = expt_bindings.expt_get_applicable_variant(self.client, context.ptr, toss); @@ -106,6 +97,18 @@ const Client = struct { return expr; } + pub fn getApplicableVariant(self: *const Client, comptime O: type, alloc: std.mem.Allocator, context: anytype, toss: i16) !O { + const context_bytes = try std.json.stringifyAlloc(alloc, context, .{}); + + const output = try self.getApplicableVariantRaw(context_bytes, toss); + + const config = try parse_json(O, alloc, output); + + freeFfiStringRaw(output); + + return config; + } + fn getSatisfiedExperimentsRaw(self: *const Client, context: []const u8, filter_prefix: []const u8) ClientError![]const u8 { const c_expr = expt_bindings.expt_get_satisfied_experiments(self.client, context.ptr, filter_prefix.ptr); @@ -118,6 +121,20 @@ const Client = struct { return expr; } + pub fn getSatisfiedExperiments(self: *const Client, comptime O: type, alloc: std.mem.Allocator, context: anytype, filter_prefix: []const []const u8) !O { + const context_bytes = try std.json.stringifyAlloc(alloc, context, .{}); + + const filter_prefix_bytes = parse_filter_keys(alloc, filter_prefix, ","); + + const output = try self.getSatisfiedExperimentsRaw(context_bytes, filter_prefix_bytes); + + const config = try parse_json(O, alloc, output); + + freeFfiStringRaw(output); + + return config; + } + fn getFilteredSatisfiedExperimentsRaw(self: *const Client, context: []const u8, filter_prefix: []const u8) ClientError![]const u8 { const c_expr = expt_bindings.expt_get_filtered_satisfied_experiments(self.client, context.ptr, filter_prefix.ptr); @@ -130,6 +147,20 @@ const Client = struct { return expr; } + pub fn getFilteredSatisfiedExperiments(self: *const Client, comptime O: type, alloc: std.mem.Allocator, context: anytype, filter_prefix: []const []const u8) !O { + const context_bytes = try std.json.stringifyAlloc(alloc, context, .{}); + + const filter_prefix_bytes = parse_filter_keys(alloc, filter_prefix, ","); + + const output = try self.getFilteredSatisfiedExperimentsRaw(context_bytes, filter_prefix_bytes); + + const config = try parse_json(O, alloc, output); + + freeFfiStringRaw(output); + + return config; + } + fn getRunningExperimentsRaw(self: *const Client) ClientError![]const u8 { const c_expr = expt_bindings.expt_get_running_experiments(self.client); @@ -141,8 +172,30 @@ const Client = struct { return expr; } + + pub fn getRunningExperiments(self: *const Client, comptime O: type, alloc: std.mem.Allocator) !O { + const output = try self.getRunningExperimentsRaw(); + + const config = try parse_json(O, alloc, output); + + freeFfiStringRaw(output); + + return config; + } }; +/// +/// +/// [`freeFfiStringRaw`] frees the memory allocated by the Expt client. +/// +/// As the memory allocations are done by the Expt client, it is important to free the memory allocated by the client. +/// +/// Used to construct safe abstractions around the raw functions. (This can be used to copy over data to a memory space allocated by the provided allocator) +/// +fn freeFfiStringRaw(s_ptr: []const u8) void { + expt_bindings.expt_free_string(s_ptr.ptr); +} + test "test init" { const config = SetupConfig{ .tenant = "public", .update_frequency = 10, .hostname = "http://localhost:8081" }; diff --git a/clients/pauli/src/root.zig b/clients/pauli/src/root.zig index 48b890f7..209e38eb 100644 --- a/clients/pauli/src/root.zig +++ b/clients/pauli/src/root.zig @@ -1,6 +1,2 @@ -const std = @import("std"); -const testing = std.testing; - -export fn add(a: i32, b: i32) i32 { - return a + b; -} +pub const cac = @import("cac.zig"); +pub const expt = @import("expt.zig"); diff --git a/clients/pauli/src/utils.zig b/clients/pauli/src/utils.zig new file mode 100644 index 00000000..3967ad26 --- /dev/null +++ b/clients/pauli/src/utils.zig @@ -0,0 +1,26 @@ +const std = @import("std"); + +pub fn parse_json(comptime T: type, alloc: std.mem.Allocator, json: []const u8) !T { + const root = try std.json.parseFromSlice(T, alloc, json, .{}); + + return root.value; +} + +pub fn parse_filter_keys(alloc: std.mem.Allocator, keys: []const []const u8, del: []const u8) ![]const u8 { + if (keys.len != 0) { + var output = std.ArrayList(u8).init(alloc); + + for (keys) |key| { + try output.appendSlice(key); + try output.appendSlice(del); + } + _ = output.orderedRemove(output.items.len - 1); + try output.append(0); + + const keys_ptr = try output.toOwnedSlice(); + + return keys_ptr; + } else { + return ""; + } +} From c4f9f6336a003c26cb7c6040833f89cd8ccfbaae Mon Sep 17 00:00:00 2001 From: Nishant Joshi Date: Sun, 14 Jul 2024 12:51:55 +0530 Subject: [PATCH 5/9] chore: fix superposition pull --- clients/pauli/build.zig | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/clients/pauli/build.zig b/clients/pauli/build.zig index 05f9a8d3..a341b9a0 100644 --- a/clients/pauli/build.zig +++ b/clients/pauli/build.zig @@ -35,6 +35,8 @@ pub fn build(b: *std.Build) void { .optimize = optimize, }); + lib_unit_tests.step.dependOn(&superposition.step); + linkBindingsDebug(b, lib_unit_tests); const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); @@ -42,6 +44,7 @@ pub fn build(b: *std.Build) void { const example_1 = b.addExecutable(.{ .name = "cli", .root_source_file = b.path("src/example_cli.zig"), .target = target, .optimize = optimize }); + example_1.step.dependOn(&superposition.step); linkBindingsDebug(b, example_1); b.installArtifact(example_1); @@ -52,8 +55,13 @@ pub fn build(b: *std.Build) void { .optimize = optimize, }); - module.addIncludePath(b.path("../../headers")); - module.addLibraryPath(b.path("../../target/debug")); + module.linkLibrary(lib); + + module.addIncludePath(b.path("superposition/headers")); + module.addLibraryPath(b.path("superposition/target/release")); + // + // module.addIncludePath(b.path("../../headers")); + // module.addLibraryPath(b.path("../../target/debug")); module.linkSystemLibrary("c", .{}); @@ -62,8 +70,8 @@ pub fn build(b: *std.Build) void { } fn linkBindingsDebug(b: *std.Build, c: *std.Build.Step.Compile) void { - c.addIncludePath(b.path("../../headers")); - c.addLibraryPath(b.path("../../target/debug")); + c.addIncludePath(b.path("superposition/headers")); + c.addLibraryPath(b.path("superposition/target/release")); c.linkLibC(); c.linkSystemLibrary("cac_client"); @@ -71,14 +79,18 @@ fn linkBindingsDebug(b: *std.Build, c: *std.Build.Step.Compile) void { } fn buildSuperpositionClient(b: *std.Build) *std.Build.Step.Run { - const repository = b.addSystemCommand(&.{ "git", "clone", "https://github.com/juspay/superposition.git" }); + const repository = b.addSystemCommand(&.{ + "sh", "-c", std.fmt.comptimePrint( + \\if [ ! -d superposition ]; then + \\ git clone https://github.com/juspay/superposition.git + \\fi + , .{}), + }); const rust_build = b.addSystemCommand(&.{ "cargo", "build", "--release" }); rust_build.setCwd(b.path("superposition")); - rust_build.addDirectoryArg(b.path("superposition")); - rust_build.step.dependOn(&repository.step); return rust_build; From 09f46ee4efba7912fbd2fe59f64e2ed698b7393d Mon Sep 17 00:00:00 2001 From: Nishant Joshi Date: Sun, 14 Jul 2024 12:53:45 +0530 Subject: [PATCH 6/9] chore: make the client ready to ship --- clients/pauli/src/example_cli.zig | 20 -------------------- clients/{pauli => zig}/.gitignore | 0 clients/{pauli => zig}/Makefile | 0 clients/{pauli => zig}/README.md | 0 clients/{pauli => zig}/build.zig | 10 ---------- clients/{pauli => zig}/build.zig.zon | 0 clients/{pauli => zig}/src/cac.zig | 0 clients/{pauli => zig}/src/expt.zig | 0 clients/{pauli => zig}/src/root.zig | 0 clients/{pauli => zig}/src/type.zig | 0 clients/{pauli => zig}/src/utils.zig | 0 11 files changed, 30 deletions(-) delete mode 100644 clients/pauli/src/example_cli.zig rename clients/{pauli => zig}/.gitignore (100%) rename clients/{pauli => zig}/Makefile (100%) rename clients/{pauli => zig}/README.md (100%) rename clients/{pauli => zig}/build.zig (85%) rename clients/{pauli => zig}/build.zig.zon (100%) rename clients/{pauli => zig}/src/cac.zig (100%) rename clients/{pauli => zig}/src/expt.zig (100%) rename clients/{pauli => zig}/src/root.zig (100%) rename clients/{pauli => zig}/src/type.zig (100%) rename clients/{pauli => zig}/src/utils.zig (100%) diff --git a/clients/pauli/src/example_cli.zig b/clients/pauli/src/example_cli.zig deleted file mode 100644 index f406331d..00000000 --- a/clients/pauli/src/example_cli.zig +++ /dev/null @@ -1,20 +0,0 @@ -const cac_client = @import("cac.zig"); -const std = @import("std"); - -pub fn main() !void { - var logging_alloc = std.heap.loggingAllocator(std.heap.page_allocator); - const alloc = logging_alloc.allocator(); - - const setup_config = cac_client.SetupConfig{ .tenant = "public", .update_frequency = 10, .hostname = "http://localhost:8080" }; - - const client = try cac_client.Client.init(setup_config); - - std.log.info("Client Initialized", .{}); - - const input = [_][]const u8{"test"}; - - const config = struct { test_2: u8 }; - - const output_1 = try client.getDefaultConfig(config, alloc, &input); - std.log.info("Output: {}", .{output_1}); -} diff --git a/clients/pauli/.gitignore b/clients/zig/.gitignore similarity index 100% rename from clients/pauli/.gitignore rename to clients/zig/.gitignore diff --git a/clients/pauli/Makefile b/clients/zig/Makefile similarity index 100% rename from clients/pauli/Makefile rename to clients/zig/Makefile diff --git a/clients/pauli/README.md b/clients/zig/README.md similarity index 100% rename from clients/pauli/README.md rename to clients/zig/README.md diff --git a/clients/pauli/build.zig b/clients/zig/build.zig similarity index 85% rename from clients/pauli/build.zig rename to clients/zig/build.zig index a341b9a0..57ca4112 100644 --- a/clients/pauli/build.zig +++ b/clients/zig/build.zig @@ -42,13 +42,6 @@ pub fn build(b: *std.Build) void { const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); test_step.dependOn(&run_lib_unit_tests.step); - const example_1 = b.addExecutable(.{ .name = "cli", .root_source_file = b.path("src/example_cli.zig"), .target = target, .optimize = optimize }); - - example_1.step.dependOn(&superposition.step); - linkBindingsDebug(b, example_1); - - b.installArtifact(example_1); - const module = b.addModule("pauli", .{ .root_source_file = b.path("src/root.zig"), .target = target, @@ -59,9 +52,6 @@ pub fn build(b: *std.Build) void { module.addIncludePath(b.path("superposition/headers")); module.addLibraryPath(b.path("superposition/target/release")); - // - // module.addIncludePath(b.path("../../headers")); - // module.addLibraryPath(b.path("../../target/debug")); module.linkSystemLibrary("c", .{}); diff --git a/clients/pauli/build.zig.zon b/clients/zig/build.zig.zon similarity index 100% rename from clients/pauli/build.zig.zon rename to clients/zig/build.zig.zon diff --git a/clients/pauli/src/cac.zig b/clients/zig/src/cac.zig similarity index 100% rename from clients/pauli/src/cac.zig rename to clients/zig/src/cac.zig diff --git a/clients/pauli/src/expt.zig b/clients/zig/src/expt.zig similarity index 100% rename from clients/pauli/src/expt.zig rename to clients/zig/src/expt.zig diff --git a/clients/pauli/src/root.zig b/clients/zig/src/root.zig similarity index 100% rename from clients/pauli/src/root.zig rename to clients/zig/src/root.zig diff --git a/clients/pauli/src/type.zig b/clients/zig/src/type.zig similarity index 100% rename from clients/pauli/src/type.zig rename to clients/zig/src/type.zig diff --git a/clients/pauli/src/utils.zig b/clients/zig/src/utils.zig similarity index 100% rename from clients/pauli/src/utils.zig rename to clients/zig/src/utils.zig From 8633683b562c1dccd1fbbcf9402126b07338b2e8 Mon Sep 17 00:00:00 2001 From: Nishant Joshi Date: Sun, 14 Jul 2024 13:14:01 +0530 Subject: [PATCH 7/9] chore: add example and readme --- clients/zig/README.md | 22 +++++++++ clients/zig/example/.gitignore | 2 + clients/zig/example/build.zig | 75 +++++++++++++++++++++++++++++++ clients/zig/example/build.zig.zon | 39 ++++++++++++++++ clients/zig/example/src/main.zig | 20 +++++++++ 5 files changed, 158 insertions(+) create mode 100644 clients/zig/example/.gitignore create mode 100644 clients/zig/example/build.zig create mode 100644 clients/zig/example/build.zig.zon create mode 100644 clients/zig/example/src/main.zig diff --git a/clients/zig/README.md b/clients/zig/README.md index cd14aa09..bc179d49 100644 --- a/clients/zig/README.md +++ b/clients/zig/README.md @@ -1,4 +1,26 @@ ### Pauli + A zig based client SDK for [`superposition`](https://github.com/juspay/superposition) service. +### Pre-requisites + +- As this client leverages `build.zig.zon` file for managing dependencies. Your zig version should atleast satisfy the minimum version mentioned in the `build.zig.zon` file. +- You should have `superposition` service running on your local machine or on a remote server. (This is optional in the initial development phase, but might be required for testing) +- The build steps consist of the following steps: + - Pulling the superposition repository, this implies that you have `git` installed on your machine and you have access to the repository. + - This is followed by the build stage of `superposition`. (required to get the shared library) `rust` and `cargo` should be installed on your machine for this stage to complete. + +### Get Started +- To get started with the client, create your own zig project: + ```bash + mkdir my_project + cd my_project + zig init-exe + ``` +- To install the client as a dependency run the following command: + ```bash + zig fetch --save + ``` + Here, the URL can be the place where the zig client is hosted or the path to the client on your local machine. + The url/path should allow the zig build system to discover the `build.zig.zon` file and the paths included within it. diff --git a/clients/zig/example/.gitignore b/clients/zig/example/.gitignore new file mode 100644 index 00000000..308b00d2 --- /dev/null +++ b/clients/zig/example/.gitignore @@ -0,0 +1,2 @@ +.zig-cache/* +zig-out/* diff --git a/clients/zig/example/build.zig b/clients/zig/example/build.zig new file mode 100644 index 00000000..7b3d6339 --- /dev/null +++ b/clients/zig/example/build.zig @@ -0,0 +1,75 @@ +const std = @import("std"); + +// Although this function looks imperative, note that its job is to +// declaratively construct a build graph that will be executed by an external +// runner. +pub fn build(b: *std.Build) void { + // Standard target options allows the person running `zig build` to choose + // what target to build for. Here we do not override the defaults, which + // means any target is allowed, and the default is native. Other options + // for restricting supported target set are available. + const target = b.standardTargetOptions(.{}); + + // Standard optimization options allow the person running `zig build` to select + // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not + // set a preferred release mode, allowing the user to decide how to optimize. + const optimize = b.standardOptimizeOption(.{}); + + const package = b.dependency("pauli", .{ + .target = target, + .optimize = optimize, + }); + + const module = package.module("pauli"); + + const exe = b.addExecutable(.{ + .name = "example", + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + exe.root_module.addImport("pauli", module); + + // This declares intent for the executable to be installed into the + // standard location when the user invokes the "install" step (the default + // step when running `zig build`). + b.installArtifact(exe); + + // This *creates* a Run step in the build graph, to be executed when another + // step is evaluated that depends on it. The next line below will establish + // such a dependency. + const run_cmd = b.addRunArtifact(exe); + + // By making the run step depend on the install step, it will be run from the + // installation directory rather than directly from within the cache directory. + // This is not necessary, however, if the application depends on other installed + // files, this ensures they will be present and in the expected location. + run_cmd.step.dependOn(b.getInstallStep()); + + // This allows the user to pass arguments to the application in the build + // command itself, like this: `zig build run -- arg1 arg2 etc` + if (b.args) |args| { + run_cmd.addArgs(args); + } + + // This creates a build step. It will be visible in the `zig build --help` menu, + // and can be selected like this: `zig build run` + // This will evaluate the `run` step rather than the default, which is "install". + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + + const exe_unit_tests = b.addTest(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); + + // Similar to creating the run step earlier, this exposes a `test` step to + // the `zig build --help` menu, providing a way for the user to request + // running the unit tests. + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_exe_unit_tests.step); +} diff --git a/clients/zig/example/build.zig.zon b/clients/zig/example/build.zig.zon new file mode 100644 index 00000000..1387b397 --- /dev/null +++ b/clients/zig/example/build.zig.zon @@ -0,0 +1,39 @@ +.{ + // This is the default name used by packages depending on this one. For + // example, when a user runs `zig fetch --save `, this field is used + // as the key in the `dependencies` table. Although the user can choose a + // different name, most users will stick with this provided value. + // + // It is redundant to include "zig" in this name because it is already + // within the Zig package namespace. + .name = "example", + + // This is a [Semantic Version](https://semver.org/). + // In a future version of Zig it will be used for package deduplication. + .version = "0.0.0", + + // This field is optional. + // This is currently advisory only; Zig does not yet do anything + // with this value. + //.minimum_zig_version = "0.11.0", + + // This field is optional. + // Each dependency must either provide a `url` and `hash`, or a `path`. + // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. + // Once all dependencies are fetched, `zig build` no longer requires + // internet connectivity. + .dependencies = .{ + .pauli = .{ + .url = "../", + .hash = "12206eb33ebbd1b0936dcb84d44460da9a863150e60e2432cef3ddf3fb6fd75a2f09", + }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + // For example... + //"LICENSE", + //"README.md", + }, +} diff --git a/clients/zig/example/src/main.zig b/clients/zig/example/src/main.zig new file mode 100644 index 00000000..27e881bf --- /dev/null +++ b/clients/zig/example/src/main.zig @@ -0,0 +1,20 @@ +const cac_client = @import("pauli").cac; +const std = @import("std"); + +pub fn main() !void { + var logging_alloc = std.heap.loggingAllocator(std.heap.page_allocator); + const alloc = logging_alloc.allocator(); + + const setup_config = cac_client.SetupConfig{ .tenant = "public", .update_frequency = 10, .hostname = "http://localhost:8080" }; + + const client = try cac_client.Client.init(setup_config); + + std.log.info("Client Initialized", .{}); + + const input = [_][]const u8{"test"}; + + const config = struct { test_2: u8 }; + + const output_1 = try client.getDefaultConfig(config, alloc, &input); + std.log.info("Output: {}", .{output_1}); +} From 0da9798adfeaa7500b8d7067cc93fa331cd87859 Mon Sep 17 00:00:00 2001 From: Nishant Joshi Date: Thu, 25 Jul 2024 16:28:39 +0530 Subject: [PATCH 8/9] chore: addressing comments --- clients/zig/build.zig | 4 ++-- clients/zig/build.zig.zon | 6 +++--- clients/zig/example/README.md | 9 +++++++++ clients/zig/example/build.zig | 6 +++--- clients/zig/example/build.zig.zon | 4 ++-- clients/zig/example/src/main.zig | 2 +- clients/zig/src/cac.zig | 4 ++-- clients/zig/src/expt.zig | 2 +- 8 files changed, 23 insertions(+), 14 deletions(-) create mode 100644 clients/zig/example/README.md diff --git a/clients/zig/build.zig b/clients/zig/build.zig index 57ca4112..d8327b03 100644 --- a/clients/zig/build.zig +++ b/clients/zig/build.zig @@ -11,7 +11,7 @@ pub fn build(b: *std.Build) void { const superposition = buildSuperpositionClient(b); const lib = b.addStaticLibrary(.{ - .name = "pauli", + .name = "superposition-client", .root_source_file = b.path("src/root.zig"), .target = target, .optimize = optimize, @@ -42,7 +42,7 @@ pub fn build(b: *std.Build) void { const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); test_step.dependOn(&run_lib_unit_tests.step); - const module = b.addModule("pauli", .{ + const module = b.addModule("superposition-client", .{ .root_source_file = b.path("src/root.zig"), .target = target, .optimize = optimize, diff --git a/clients/zig/build.zig.zon b/clients/zig/build.zig.zon index 4c53da6b..491b46c6 100644 --- a/clients/zig/build.zig.zon +++ b/clients/zig/build.zig.zon @@ -6,16 +6,16 @@ // // It is redundant to include "zig" in this name because it is already // within the Zig package namespace. - .name = "pauli", + .name = "superposition-client", // This is a [Semantic Version](https://semver.org/). // In a future version of Zig it will be used for package deduplication. - .version = "0.0.0", + .version = "0.1.0", // This field is optional. // This is currently advisory only; Zig does not yet do anything // with this value. - //.minimum_zig_version = "0.11.0", + .minimum_zig_version = "0.11.0", // This field is optional. // Each dependency must either provide a `url` and `hash`, or a `path`. diff --git a/clients/zig/example/README.md b/clients/zig/example/README.md new file mode 100644 index 00000000..5a696538 --- /dev/null +++ b/clients/zig/example/README.md @@ -0,0 +1,9 @@ +### Example + +### Pre-requisites + +To add/update the dependency, run the following command: + +```zig +zig fetch --save ../ +``` diff --git a/clients/zig/example/build.zig b/clients/zig/example/build.zig index 7b3d6339..93b6bd31 100644 --- a/clients/zig/example/build.zig +++ b/clients/zig/example/build.zig @@ -15,12 +15,12 @@ pub fn build(b: *std.Build) void { // set a preferred release mode, allowing the user to decide how to optimize. const optimize = b.standardOptimizeOption(.{}); - const package = b.dependency("pauli", .{ + const package = b.dependency("superposition-client", .{ .target = target, .optimize = optimize, }); - const module = package.module("pauli"); + const module = package.module("superposition-client"); const exe = b.addExecutable(.{ .name = "example", @@ -29,7 +29,7 @@ pub fn build(b: *std.Build) void { .optimize = optimize, }); - exe.root_module.addImport("pauli", module); + exe.root_module.addImport("superposition-client", module); // This declares intent for the executable to be installed into the // standard location when the user invokes the "install" step (the default diff --git a/clients/zig/example/build.zig.zon b/clients/zig/example/build.zig.zon index 1387b397..dfec841b 100644 --- a/clients/zig/example/build.zig.zon +++ b/clients/zig/example/build.zig.zon @@ -23,9 +23,9 @@ // Once all dependencies are fetched, `zig build` no longer requires // internet connectivity. .dependencies = .{ - .pauli = .{ + .@"superposition-client" = .{ .url = "../", - .hash = "12206eb33ebbd1b0936dcb84d44460da9a863150e60e2432cef3ddf3fb6fd75a2f09", + .hash = "1220c89682bac3ca4efa3c6bdf1fb2472b66a2d5659f2508c8ebd21609ffa68646c4", }, }, .paths = .{ diff --git a/clients/zig/example/src/main.zig b/clients/zig/example/src/main.zig index 27e881bf..90293892 100644 --- a/clients/zig/example/src/main.zig +++ b/clients/zig/example/src/main.zig @@ -5,7 +5,7 @@ pub fn main() !void { var logging_alloc = std.heap.loggingAllocator(std.heap.page_allocator); const alloc = logging_alloc.allocator(); - const setup_config = cac_client.SetupConfig{ .tenant = "public", .update_frequency = 10, .hostname = "http://localhost:8080" }; + const setup_config = cac_client.SetupConfig{ .tenant = "dev", .update_frequency = 10, .hostname = "http://localhost:8080" }; const client = try cac_client.Client.init(setup_config); diff --git a/clients/zig/src/cac.zig b/clients/zig/src/cac.zig index 77362113..3f5bb579 100644 --- a/clients/zig/src/cac.zig +++ b/clients/zig/src/cac.zig @@ -193,14 +193,14 @@ fn freeFfiStringRaw(_: []const u8) void { // --------------------------------------------------------------------------- test "test init" { - const config = SetupConfig{ .tenant = "public", .update_frequency = 10, .hostname = "http://localhost:8080" }; + const config = SetupConfig{ .tenant = "dev", .update_frequency = 10, .hostname = "http://localhost:8080" }; const client = try Client.init(config); defer client.deinit(); } test "test lastModifiedRaw" { - const config = SetupConfig{ .tenant = "public", .update_frequency = 10, .hostname = "http://localhost:8080" }; + const config = SetupConfig{ .tenant = "dev", .update_frequency = 10, .hostname = "http://localhost:8080" }; const client = try Client.init(config); defer client.deinit(); diff --git a/clients/zig/src/expt.zig b/clients/zig/src/expt.zig index 36b5fdce..7cb022a0 100644 --- a/clients/zig/src/expt.zig +++ b/clients/zig/src/expt.zig @@ -197,7 +197,7 @@ fn freeFfiStringRaw(s_ptr: []const u8) void { } test "test init" { - const config = SetupConfig{ .tenant = "public", .update_frequency = 10, .hostname = "http://localhost:8081" }; + const config = SetupConfig{ .tenant = "dev", .update_frequency = 10, .hostname = "http://localhost:8081" }; const client = try Client.init(config); defer client.deinit(); From 9f9d84c001a3995c642fac674c54a0b685787257 Mon Sep 17 00:00:00 2001 From: Nishant Joshi Date: Mon, 29 Jul 2024 11:28:03 +0530 Subject: [PATCH 9/9] chore: add flakedeps for zig --- flake.nix | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/flake.nix b/flake.nix index 9c06c9a8..9a57589c 100644 --- a/flake.nix +++ b/flake.nix @@ -5,6 +5,10 @@ haskell-flake.url = "github:srid/haskell-flake"; systems.url = "github:nix-systems/default"; pre-commit-hooks.url = "github:cachix/pre-commit-hooks.nix"; + zig = { + url = "github:ziglang/zig"; + inputs.nixpkgs.follows = "nixpkgs"; + }; crane = { url = "github:ipetkov/crane"; inputs.nixpkgs.follows = "nixpkgs"; @@ -44,6 +48,8 @@ jq nodejs_18 nixpkgs-fmt + zig_0_12 + zls ]; }; };