Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Client] add swift client #178

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions clients/swift/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
## CAC and Experimentation clients in `Swift`

- ## Walkthrough
- Create bridging file for each module
- add the `#include` statement for the C header file
- without xcode: use `swiftc` to compile modules using `-L`, `-l` and `-import-objc-header` flags to specify object files search dir, object-module name and bridging file path respectively. (checkout : [default.nix](default.nix))
- with xcode: todo!

- ## setup (nix) :
`nix develop .#swift`

- ## run without xcode (using nix) :
1. to compile cac client: `compileCac`
2. to compile exp client: `compileExp`
3. use generated bins

- ## run using xcode :
todo!

- ## run example :
todo!
6 changes: 6 additions & 0 deletions clients/swift/cac/Bridging-Header.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#ifndef CAC_Bridging_Header_h
#define CAC_Bridging_Header_h

#include "../../../headers/libcac_client.h"

#endif /* CAC_Bridging_Header_h */
93 changes: 93 additions & 0 deletions clients/swift/cac/main.swift
divinenaman marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import Foundation

typealias UnknownClientPointer = OpaquePointer
typealias Value = [String: Any]

enum MergeStrategy {
case MERGE
case REPLACE

var show: String {
switch self {
case .MERGE:
return "MERGE"
case .REPLACE:
return "REPLACE"
}
}
}

func createCacClient(tenant: String, frequency: UInt, hostname: String) -> Bool {
return tenant.withCString { t -> Bool in
return hostname.withCString { h -> Bool in
let resp = cac_new_client(t, frequency, h);
if (resp == 0) {
return true
}
return false
}
}
}

func getCacClient(tenant: String) -> UnknownClientPointer? {
return tenant.withCString { t -> UnknownClientPointer? in
return cac_get_client(tenant)
divinenaman marked this conversation as resolved.
Show resolved Hide resolved
}
}

func cacStartPolling(tenant: String) {
tenant.withCString { t in
cac_start_polling_update(t)
}
}

func getCacLastModified(client: UnknownClientPointer) -> String? {
let resp = cac_get_last_modified(client)
return resp.map { String(cString: $0) }
}

func parseJson(jsonString: String) -> Value? {
if let jsonData = jsonString.data(using: .utf8) {
do {
return try JSONSerialization.jsonObject(with: jsonData, options: []) as? Value
} catch {
return nil
}
}
return nil
}

func getResolvedConfig(client: UnknownClientPointer, context: String, filterKeys: [String]? = nil) -> Value? {
let keys = filterKeys.map { $0.joined(separator: "|") }

return context.withCString { c -> Value? in
return MergeStrategy.MERGE.show.withCString { m -> Value? in
let rawData : UnsafePointer<CChar>?
if let k = keys {
rawData = k.withCString { ck -> UnsafePointer<CChar>? in
return cac_get_resolved_config(client, c, ck, m)
}
} else {
rawData = cac_get_resolved_config(client, c, nil, m)
}
return rawData.map { String(cString: $0) }.flatMap { parseJson(jsonString: $0) }
}
}
}

func getDefaultConfig(client: UnknownClientPointer, filterKeys: [String]) -> Value? {
divinenaman marked this conversation as resolved.
Show resolved Hide resolved
let keys = filterKeys.joined(separator: "|")

return keys.withCString { k -> Value? in
let rawData = cac_get_default_config(client, keys)
return rawData.map { String(cString: $0) }.flatMap { parseJson(jsonString: $0) }
}
}

func cacFreeClient(client: UnknownClientPointer) {
cac_free_client(client)
}

func cacLastErrorMessage() -> String? {
return cac_last_error_message().map { String(cString: $0) }
}
21 changes: 21 additions & 0 deletions clients/swift/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
perSystem = { config, pkgs, self', lib, ... }: {
devShells.swift = let
compileCac = pkgs.writeShellScriptBin "compileCac" ''
swiftc cac/main.swift -L../../target/debug -lcac_client -import-objc-header cac/Bridging-Header.h -o cac-swift
'';
compileExp = pkgs.writeShellScriptBin "compileExp" ''
swiftc exp/main.swift example/exp_example.swift -L../../target/debug -lexperimentation_client -import-objc-header exp/Bridging-Header.h -o exp-swift
'';
in
pkgs.mkShell {
name = "superposition-swift-clients";
buildInputs = with pkgs; [
swift
swiftPackages.Foundation
compileCac
compileExp
];
};
};
}
41 changes: 41 additions & 0 deletions clients/swift/example/cac_example.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import Foundation

func main() {
let t = "test"
let f : UInt = 300
let h = "http://localhost:8080"

if (createCacClient(tenant: t, frequency: f, hostname: h)) {
print("createCacClient success!")

if let client = getCacClient(tenant: t) {
print("getCacClient success!")

let thread = Thread {
cacStartPolling(tenant: t)
}
thread.start()
print("cacStartPolling: thread started!")

let m = getCacLastModified(client: client)
let r = getResolvedConfig(client: client, context: "{}")
let d = getDefaultConfig(client: client, filterKeys: [])

print("Last Modified: \(m)")
print("Resolved Config: \(r)")
print("Default Config: \(d)")

thread.cancel()
print("cacStartPolling: thread closed!")

cacFreeClient(client: client)
print("Free client memory!")
} else {
print("getCacClient failed!")
}
} else {
print("createCacClient failed!")
}
}

main()
39 changes: 39 additions & 0 deletions clients/swift/example/exp_example.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Foundation

func main() {
let t = "test"
let f : UInt = 300
let h = "http://localhost:8080"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change the port to say 9090 or something else, since superposition will by default in dev env runs on 8080

Copy link
Author

@divinenaman divinenaman Jul 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ShubhranshuSanjeev needed to point to superposition server only, these are basic test


if (createExptClient(tenant: t, frequency: f, hostname: h)) {
print("createExptClient successs!")
if let client = getExptClient(tenant: t) {
print("getExptClient successs!")

let thread = Thread {
exptStartPolling(tenant: t)
}
thread.start()
print("cacStartPolling: thread started!")

let m = getApplicableVariant(client: client, context: "{}", toss: 1)
let r = getSatisfiedExperiments(client: client, context: "{}", filterPrefix: [])
let d = getRunningExperiments(client: client)
let v = getFilteredSatisfiedExperiments(client: client, context: "{}", filterPrefix: [])
print("Applicable Variants: \(r)")
print("Satisfied Experiments: \(m)")
print("Running Experiments: \(d)")
print("Filtered Satisfied Experiments: \(v)")

thread.cancel()
print("cacStartPolling: thread closed!")

exptFreeClient(client: client)
print("Free client memory!")
} else {
print("getExptClient failed!")
}
} else {
print("createExptClient failed!")
}
}
6 changes: 6 additions & 0 deletions clients/swift/exp/Bridging-Header.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#ifndef Experimentation_Bridging_Header_h
#define Experimentation_Bridging_Header_h

#include "../../../headers/libexperimentation_client.h"

#endif /* Experimentation_Bridging_Header_h */
81 changes: 81 additions & 0 deletions clients/swift/exp/main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import Foundation

typealias UnknownClientPointer = OpaquePointer
typealias Value = [String: Any]

func createExptClient(tenant: String, frequency: UInt, hostname: String) -> Bool {
return tenant.withCString { t -> Bool in
return hostname.withCString { h -> Bool in
let resp = expt_new_client(t, frequency, h);
if (resp == 0) {
return true
}
return false
}
}
}

func getExptClient(tenant: String) -> UnknownClientPointer? {
return tenant.withCString { t -> UnknownClientPointer? in
return expt_get_client(tenant)
}
}

func exptStartPolling(tenant: String) {
tenant.withCString { t in
expt_start_polling_update(t)
}
}

func parseJson(jsonString: String) -> Value? {
if let jsonData = jsonString.data(using: .utf8) {
do {
return try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any]
} catch {
return nil
}
}
return nil
}

func getApplicableVariant(client: UnknownClientPointer, context: String, toss: Int16) -> Value? {
return context.withCString { c -> Value? in
let rawData = expt_get_applicable_variant(client, c, toss)
return rawData.map { String(cString: $0) }.flatMap { parseJson(jsonString: $0) }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the use of flatMap here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
}

func getSatisfiedExperiments(client: UnknownClientPointer, context: String, filterPrefix: [String]) -> Value? {
divinenaman marked this conversation as resolved.
Show resolved Hide resolved
let keys = filterPrefix.joined(separator: "|")

return keys.withCString { k -> Value? in
return context.withCString { c -> Value? in
let rawData = expt_get_satisfied_experiments(client, c, k)
return rawData.map { String(cString: $0) }.flatMap { parseJson(jsonString: $0) }
}
}
}

func getRunningExperiments(client: UnknownClientPointer) -> Value? {
let resp = expt_get_running_experiments(client)
return resp.map { String(cString: $0) }.flatMap { parseJson(jsonString: $0) }
}


func getFilteredSatisfiedExperiments(client: UnknownClientPointer, context: String, filterPrefix: [String]) -> Value? {
let keys = filterPrefix.joined(separator: "|")
return context.withCString { c -> Value? in
return keys.withCString { k -> Value? in
let resp = expt_get_filtered_satisfied_experiments(client, c, k)
return resp.map { String(cString: $0) }.flatMap { parseJson(jsonString: $0) }
}
}
}

func exptFreeClient(client: UnknownClientPointer) {
expt_free_client(client)
}

func exptLastErrorMessage() -> String? {
return expt_last_error_message().map { String(cString: $0) }
}
1 change: 1 addition & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
./nix/pre-commit.nix
./clients/haskell
./nix/rust.nix
./clients/swift
];

perSystem = { pkgs, self', config, ... }: {
Expand Down