Skip to content

Commit

Permalink
feat: incorporate error handling from Rust core over FFI to clients (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
yquansah authored Nov 16, 2023
1 parent 3e93117 commit 08d7a28
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 66 deletions.
51 changes: 43 additions & 8 deletions sdk/client/engine/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,47 @@ impl Engine {
}
}

fn result_to_json_ptr<T: Serialize>(result: T) -> *mut c_char {
let json_string = serde_json::to_string(&result).unwrap();
#[derive(Serialize)]
struct FFIResponse<T>
where
T: Serialize,
{
status: Status,
result: Option<T>,
error_message: Option<String>,
}

#[derive(Serialize)]
enum Status {
#[serde(rename = "success")]
Success,
#[serde(rename = "failure")]
Failure,
}

impl<T> From<Result<T, Whatever>> for FFIResponse<T>
where
T: Serialize,
{
fn from(value: Result<T, Whatever>) -> Self {
match value {
Ok(result) => FFIResponse {
status: Status::Success,
result: Some(result),
error_message: None,
},
Err(e) => FFIResponse {
status: Status::Failure,
result: None,
error_message: Some(e.to_string()),
},
}
}
}

fn result_to_json_ptr<T: Serialize>(result: Result<T, Whatever>) -> *mut c_char {
let ffi_response: FFIResponse<T> = result.into();
let json_string = serde_json::to_string(&ffi_response).unwrap();
CString::new(json_string).unwrap().into_raw()
}

Expand Down Expand Up @@ -119,9 +158,7 @@ pub unsafe extern "C" fn variant(
let e = get_engine(engine_ptr).unwrap();
let e_req = get_evaluation_request(evaluation_request);

let variant_response = e.variant(&e_req).unwrap();

result_to_json_ptr(variant_response)
result_to_json_ptr(e.variant(&e_req))
}

/// # Safety
Expand All @@ -135,9 +172,7 @@ pub unsafe extern "C" fn boolean(
let e = get_engine(engine_ptr).unwrap();
let e_req = get_evaluation_request(evaluation_request);

let boolean_response = e.boolean(&e_req).unwrap();

result_to_json_ptr(boolean_response)
result_to_json_ptr(e.boolean(&e_req))
}

unsafe fn get_evaluation_request(
Expand Down
21 changes: 5 additions & 16 deletions sdk/client/go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,35 +31,24 @@ package main

import (
"context"
"encoding/json"
"fmt"
"log"

evaluation "go.flipt.io/flipt/flipt-client-go"
)

func main() {
evaluationClient := evaluation.NewClient("default")
// You can initialize the client with a namespace using "WithNamespace", otherwise
// it will target the default namespace.
evaluationClient := evaluation.NewClient(evaluation.WithNamespace("staging"))

evalCtx := map[string]string{
variantResult, err := evaluationClient.Variant(context.Background(), "flag1", "someentity", map[string]string{
"fizz": "buzz",
}

evalCtxBytes, err := json.Marshal(evalCtx)
if err != nil {
log.Fatal(err)
}

variantEvaluationResponse, err := evaluationClient.Variant(context.Background(), &evaluation.EvaluationRequest{
NamespaceKey: "default",
FlagKey: "flag1",
EntityId: "someentity",
Context: string(evalCtxBytes),
})
if err != nil {
log.Fatal(err)
}

fmt.Println(variantEvaluationResponse)
fmt.Println(*variantResult.Result)
}
```
71 changes: 55 additions & 16 deletions sdk/client/go/flipt-client-go/evaluation.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,23 @@ import (
)

// Client wraps the functionality of making variant and boolean evaluation of Flipt feature flags
// using an engine that is compiled to a dynamic linking library.
// using an engine that is compiled to a dynamically linked library.
type Client struct {
engine unsafe.Pointer
engine unsafe.Pointer
namespace string
}

// NewClient constructs an Client.
func NewClient(namespace string) *Client {
ns := []*C.char{C.CString(namespace)}
func NewClient(opts ...clientOption) *Client {
client := &Client{
namespace: "default",
}

for _, opt := range opts {
opt(client)
}

ns := []*C.char{C.CString(client.namespace)}

// Free the memory of the C Strings that were created to initialize the engine.
defer func() {
Expand All @@ -41,14 +50,34 @@ func NewClient(namespace string) *Client {

eng := C.initialize_engine(nsPtr)

return &Client{
engine: eng,
client.engine = eng

return client
}

// clientOption adds additional configuraiton for Client parameters
type clientOption func(*Client)

// WithNamespace allows for specifying which namespace the clients wants to make evaluations from.
func WithNamespace(namespace string) clientOption {
return func(c *Client) {
c.namespace = namespace
}
}

// Variant makes an evaluation on a variant flag using the allocated Rust engine.
func (e *Client) Variant(_ context.Context, evaluationRequest *EvaluationRequest) (*VariantEvaluationResponse, error) {
ereq, err := json.Marshal(evaluationRequest)
func (e *Client) Variant(_ context.Context, flagKey, entityID string, evalContext map[string]string) (*VariantResult, error) {
eb, err := json.Marshal(evalContext)
if err != nil {
return nil, err
}

ereq, err := json.Marshal(evaluationRequest{
NamespaceKey: e.namespace,
FlagKey: flagKey,
EntityId: entityID,
Context: string(eb),
})
if err != nil {
return nil, err
}
Expand All @@ -58,18 +87,28 @@ func (e *Client) Variant(_ context.Context, evaluationRequest *EvaluationRequest

b := C.GoBytes(unsafe.Pointer(variant), (C.int)(C.strlen(variant)))

var ver *VariantEvaluationResponse
var vr *VariantResult

if err := json.Unmarshal(b, &ver); err != nil {
if err := json.Unmarshal(b, &vr); err != nil {
return nil, err
}

return ver, nil
return vr, nil
}

// Boolean makes an evaluation on a boolean flag using the allocated Rust engine.
func (e *Client) Boolean(_ context.Context, evaluationRequest *EvaluationRequest) (*BooleanEvaluationResponse, error) {
ereq, err := json.Marshal(evaluationRequest)
func (e *Client) Boolean(_ context.Context, flagKey, entityID string, evalContext map[string]string) (*BooleanResult, error) {
eb, err := json.Marshal(evalContext)
if err != nil {
return nil, err
}

ereq, err := json.Marshal(evaluationRequest{
NamespaceKey: e.namespace,
FlagKey: flagKey,
EntityId: entityID,
Context: string(eb),
})
if err != nil {
return nil, err
}
Expand All @@ -79,13 +118,13 @@ func (e *Client) Boolean(_ context.Context, evaluationRequest *EvaluationRequest

b := C.GoBytes(unsafe.Pointer(boolean), (C.int)(C.strlen(boolean)))

var ber *BooleanEvaluationResponse
var br *BooleanResult

if err := json.Unmarshal(b, &ber); err != nil {
if err := json.Unmarshal(b, &br); err != nil {
return nil, err
}

return ber, nil
return br, nil
}

// Close cleans up the allocated engine as it was initialized in the constructor.
Expand Down
14 changes: 13 additions & 1 deletion sdk/client/go/flipt-client-go/models.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package evaluation

type EvaluationRequest struct {
type evaluationRequest struct {
NamespaceKey string `json:"namespace_key"`
FlagKey string `json:"flag_key"`
EntityId string `json:"entity_id"`
Expand All @@ -25,3 +25,15 @@ type BooleanEvaluationResponse struct {
RequestDurationMillis float64 `json:"request_duration_millis"`
Timestamp string `json:"timestamp"`
}

type VariantResult struct {
Status string `json:"status"`
Result *VariantEvaluationResponse `json:"result,omitempty"`
ErrorMessage string `json:"error_message,omitempty"`
}

type BooleanResult struct {
Status string `json:"status"`
Result *BooleanEvaluationResponse `json:"result,omitempty"`
ErrorMessage string `json:"error_message,omitempty"`
}
7 changes: 5 additions & 2 deletions sdk/client/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ In your Python code you can import this client and use it as so:
```python
from flipt_client_python import FliptEvaluationClient

flipt_evaluation_client = FliptEvaluationClient(namespaces=["default", "another-namespace"])
# namespace_key is optional here and will have a value of "default" if not specified
flipt_evaluation_client = FliptEvaluationClient(namespace="staging")

variant_response = flipt_evaluation_client.variant(namespace_key="default", flag_key="flag1", entity_id="entity", context={"this": "context"})
variant_result = flipt_evaluation_client.variant(flag_key="flag1", entity_id="entity", context={"fizz": "buzz"})

print(variant_result)
```
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,20 @@
import os

from .models import (
BooleanEvaluationResponse,
BooleanResult,
EvaluationRequest,
VariantEvaluationResponse,
VariantResult,
)


class FliptEvaluationClient:
def __init__(self, namespaces: list[str]):
def __init__(self, namespace: str = "default"):
engine_library_path = os.environ.get("ENGINE_LIB_PATH")
if engine_library_path is None:
raise Exception("ENGINE_LIB_PATH not set")

self.namespace_key = namespace

self.ffi_core = ctypes.CDLL(engine_library_path)

self.ffi_core.initialize_engine.restype = ctypes.c_void_p
Expand All @@ -26,46 +28,44 @@ def __init__(self, namespaces: list[str]):
self.ffi_core.boolean.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
self.ffi_core.boolean.restype = ctypes.c_char_p

ns = (ctypes.c_char_p * len(namespaces))()
ns[:] = [s.encode("utf-8") for s in namespaces]
namespace_list = [namespace]

ns = (ctypes.c_char_p * len(namespace_list))()
ns[:] = [s.encode("utf-8") for s in namespace_list]

self.engine = self.ffi_core.initialize_engine(ns)

def __del__(self):
if hasattr(self, "engine") and self.engine is not None:
self.destroy_engine(self.engine)
self.ffi_core.destroy_engine(self.engine)

def variant(
self, namespace_key: str, flag_key: str, entity_id: str, context: dict
) -> VariantEvaluationResponse:
def variant(self, flag_key: str, entity_id: str, context: dict) -> VariantResult:
response = self.ffi_core.variant(
self.engine,
serialize_evaluation_request(namespace_key, flag_key, entity_id, context),
serialize_evaluation_request(
self.namespace_key, flag_key, entity_id, context
),
)

bytes_returned = ctypes.c_char_p(response).value

variant_evaluation_response = VariantEvaluationResponse.parse_raw(
bytes_returned
)
variant_result = VariantResult.parse_raw(bytes_returned)

return variant_evaluation_response
return variant_result

def boolean(
self, namespace_key: str, flag_key: str, entity_id: str, context: dict
) -> BooleanEvaluationResponse:
def boolean(self, flag_key: str, entity_id: str, context: dict) -> BooleanResult:
response = self.ffi_core.boolean(
self.engine,
serialize_evaluation_request(namespace_key, flag_key, entity_id, context),
serialize_evaluation_request(
self.namespace_key, flag_key, entity_id, context
),
)

bytes_returned = ctypes.c_char_p(response).value

boolean_evaluation_response = BooleanEvaluationResponse.parse_raw(
bytes_returned
)
boolean_result = BooleanResult.parse_raw(bytes_returned)

return boolean_evaluation_response
return boolean_result


def serialize_evaluation_request(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from pydantic import BaseModel
from typing import List
from typing import List, Optional


class EvaluationRequest(BaseModel):
Expand All @@ -26,3 +26,15 @@ class BooleanEvaluationResponse(BaseModel):
reason: str
request_duration_millis: float
timestamp: str


class VariantResult(BaseModel):
status: str
result: Optional[VariantEvaluationResponse] = None
error_message: Optional[str] = None


class BooleanResult(BaseModel):
status: str
result: Optional[BooleanEvaluationResponse] = None
error_message: Optional[str] = None

0 comments on commit 08d7a28

Please sign in to comment.