From 2d31ac0012f474c9d17ba1d9fd328cd15e941f5b Mon Sep 17 00:00:00 2001 From: hroederld Date: Thu, 20 Feb 2020 15:13:08 -0800 Subject: [PATCH 001/161] [ch66649] initial copy from moonshot (and minor template fill in) --- CONTRIBUTING.md | 14 ++ README.md | 49 ++++++- launchdarkly.lua | 368 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 430 insertions(+), 1 deletion(-) create mode 100644 CONTRIBUTING.md create mode 100644 launchdarkly.lua diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..cb41d17 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,14 @@ +Contributing to the LaunchDarkly Server SDK for Lua +================================================ + +LaunchDarkly has published an [SDK contributor's guide](https://docs.launchdarkly.com/docs/sdk-contributors-guide) that provides a detailed explanation of how our SDKs work. See below for additional information on how to contribute to this SDK. + +Submitting bug reports and feature requests +------------------ + +The LaunchDarkly SDK team monitors the [issue tracker](https://github.com/launchdarkly/lua-server-sdk/issues) in the SDK repository. Bug reports and feature requests specific to this SDK should be filed in this issue tracker. The SDK team will respond to all newly filed issues within two business days. + +Submitting pull requests +------------------ + +We encourage pull requests and other contributions from the community. Before submitting pull requests, ensure that all temporary or unintended code is removed. Don't worry about adding reviewers to the pull request; the LaunchDarkly SDK team will add themselves. The SDK team will acknowledge all pull requests within two business days. diff --git a/README.md b/README.md index fa37edb..fb7f090 100644 --- a/README.md +++ b/README.md @@ -1 +1,48 @@ -# lua-server-sdk-private \ No newline at end of file +LaunchDarkly server SDK for Lua +=========================== + +[![Circle CI](https://circleci.com/gh/launchdarkly/lua-server-sdk.svg?style=shield)](https://circleci.com/gh/launchdarkly/lua-server-sdk) + +*This version of the SDK is a **beta** version and should not be considered ready for production use while this message is visible.* + +LaunchDarkly overview +------------------------- +[LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves over 100 billion feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/docs/getting-started) using LaunchDarkly today! + +[![Twitter Follow](https://img.shields.io/twitter/follow/launchdarkly.svg?style=social&label=Follow&maxAge=2592000)](https://twitter.com/intent/follow?screen_name=launchdarkly) + +Getting started +----------- + +Refer to the [SDK documentation](https://docs.launchdarkly.com/docs/lua-server-reference#section-getting-started) for instructions on getting started with using the SDK. + +Learn more +----------- + +Check out our [documentation](https://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. You can also head straight to the [complete reference guide for this SDK](https://docs.launchdarkly.com/docs/lua-server-reference). + +Testing +------- + +We run integration tests for all our SDKs using a centralized test harness. This approach gives us the ability to test for consistency across SDKs, as well as test networking behavior in a long-running application. These tests cover each method in the SDK, and verify that event sending, flag evaluation, stream reconnection, and other aspects of the SDK all behave correctly. + +Contributing +------------ + +We encourage pull requests and other contributions from the community. Check out our [contributing guidelines](CONTRIBUTING.md) for instructions on how to contribute to this SDK. + +About LaunchDarkly +----------- + +* LaunchDarkly is a continuous delivery platform that provides feature flags as a service and allows developers to iterate quickly and safely. We allow you to easily flag your features and manage them from the LaunchDarkly dashboard. With LaunchDarkly, you can: + * Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases. + * Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?). + * Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file. + * Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). Disable parts of your application to facilitate maintenance, without taking everything offline. +* LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Check out [our documentation](https://docs.launchdarkly.com/docs) for a complete list. +* Explore LaunchDarkly + * [launchdarkly.com](https://www.launchdarkly.com/ "LaunchDarkly Main Website") for more information + * [docs.launchdarkly.com](https://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and SDK reference guides + * [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/ "LaunchDarkly API Documentation") for our API documentation + * [blog.launchdarkly.com](https://blog.launchdarkly.com/ "LaunchDarkly Blog Documentation") for the latest product updates + * [Feature Flagging Guide](https://github.com/launchdarkly/featureflags/ "Feature Flagging Guide") for best practices and strategies diff --git a/launchdarkly.lua b/launchdarkly.lua new file mode 100644 index 0000000..6468fec --- /dev/null +++ b/launchdarkly.lua @@ -0,0 +1,368 @@ +local ffi = require("ffi") +local cjson = require("cjson") + +local ld = {} + +ffi.cdef[[ + struct LDJSON; + typedef enum { + LDNull = 0, + LDText, + LDNumber, + LDBool, + LDObject, + LDArray + } LDJSONType; + struct LDJSON * LDNewNull(); + struct LDJSON * LDNewBool(const bool boolean); + struct LDJSON * LDNewNumber(const double number); + struct LDJSON * LDNewText(const char *const text); + struct LDJSON * LDNewObject(); + struct LDJSON * LDNewArray(); + bool LDSetNumber(struct LDJSON *const node, const double number); + void LDJSONFree(struct LDJSON *const json); + struct LDJSON * LDJSONDuplicate(const struct LDJSON *const json); + LDJSONType LDJSONGetType(const struct LDJSON *const json); + bool LDJSONCompare(const struct LDJSON *const left, + const struct LDJSON *const right); + bool LDGetBool(const struct LDJSON *const node); + double LDGetNumber(const struct LDJSON *const node); + const char * LDGetText(const struct LDJSON *const node); + struct LDJSON * LDIterNext(const struct LDJSON *const iter); + struct LDJSON * LDGetIter(const struct LDJSON *const collection); + const char * LDIterKey(const struct LDJSON *const iter); + unsigned int LDCollectionGetSize( + const struct LDJSON *const collection); + struct LDJSON * LDCollectionDetachIter( + struct LDJSON *const collection, struct LDJSON *const iter); + struct LDJSON * LDArrayLookup(const struct LDJSON *const array, + const unsigned int index); + bool LDArrayPush(struct LDJSON *const array, + struct LDJSON *const item); + bool LDArrayAppend(struct LDJSON *const prefix, + const struct LDJSON *const suffix); + struct LDJSON * LDObjectLookup(const struct LDJSON *const object, + const char *const key); + bool LDObjectSetKey(struct LDJSON *const object, + const char *const key, struct LDJSON *const item); + void LDObjectDeleteKey(struct LDJSON *const object, + const char *const key); + struct LDJSON * LDObjectDetachKey(struct LDJSON *const object, + const char *const key); + bool LDObjectMerge(struct LDJSON *const to, + const struct LDJSON *const from); + char * LDJSONSerialize(const struct LDJSON *const json); + struct LDJSON * LDJSONDeserialize(const char *const text); + struct LDStoreInterface; struct LDConfig; + struct LDConfig * LDConfigNew(const char *const key); + void LDConfigFree(struct LDConfig *const config); + bool LDConfigSetBaseURI(struct LDConfig *const config, + const char *const baseURI); + bool LDConfigSetStreamURI(struct LDConfig *const config, + const char *const streamURI); + bool LDConfigSetEventsURI(struct LDConfig *const config, + const char *const eventsURI); + void LDConfigSetStream(struct LDConfig *const config, + const bool stream); + void LDConfigSetSendEvents(struct LDConfig *const config, + const bool sendEvents); + void LDConfigSetEventsCapacity(struct LDConfig *const config, + const unsigned int eventsCapacity); + void LDConfigSetTimeout(struct LDConfig *const config, + const unsigned int milliseconds); + void LDConfigSetFlushInterval(struct LDConfig *const config, + const unsigned int milliseconds); + void LDConfigSetPollInterval(struct LDConfig *const config, + const unsigned int milliseconds); + void LDConfigSetOffline(struct LDConfig *const config, + const bool offline); + void LDConfigSetUseLDD(struct LDConfig *const config, + const bool useLDD); + void LDConfigSetAllAttributesPrivate(struct LDConfig *const config, + const bool allAttributesPrivate); + void LDConfigInlineUsersInEvents(struct LDConfig *const config, + const bool inlineUsersInEvents); + void LDConfigSetUserKeysCapacity(struct LDConfig *const config, + const unsigned int userKeysCapacity); + void LDConfigSetUserKeysFlushInterval(struct LDConfig *const config, + const unsigned int milliseconds); + bool LDConfigAddPrivateAttribute(struct LDConfig *const config, + const char *const attribute); + void LDConfigSetFeatureStoreBackend(struct LDConfig *const config, + struct LDStoreInterface *const backend); + struct LDUser * LDUserNew(const char *const userkey); + void LDUserFree(struct LDUser *const user); + void LDUserSetAnonymous(struct LDUser *const user, const bool anon); + bool LDUserSetIP(struct LDUser *const user, const char *const ip); + bool LDUserSetFirstName(struct LDUser *const user, + const char *const firstName); + bool LDUserSetLastName(struct LDUser *const user, + const char *const lastName); + bool LDUserSetEmail(struct LDUser *const user, + const char *const email); + bool LDUserSetName(struct LDUser *const user, + const char *const name); + bool LDUserSetAvatar(struct LDUser *const user, + const char *const avatar); + bool LDUserSetCountry(struct LDUser *const user, + const char *const country); + bool LDUserSetSecondary(struct LDUser *const user, + const char *const secondary); + void LDUserSetCustom(struct LDUser *const user, + struct LDJSON *const custom); + bool LDUserAddPrivateAttribute(struct LDUser *const user, + const char *const attribute); + struct LDClient * LDClientInit(struct LDConfig *const config, + const unsigned int maxwaitmilli); + void LDClientClose(struct LDClient *const client); + bool LDClientIsInitialized(struct LDClient *const client); + bool LDClientTrack(struct LDClient *const client, + const char *const key, const struct LDUser *const user, + struct LDJSON *const data); + bool LDClientTrackMetric(struct LDClient *const client, + const char *const key, const struct LDUser *const user, + struct LDJSON *const data, const double metric); + bool LDClientIdentify(struct LDClient *const client, + const struct LDUser *const user); + bool LDClientIsOffline(struct LDClient *const client); + void LDClientFlush(struct LDClient *const client); + void * LDAlloc(const size_t bytes); + void LDFree(void *const buffer); + char * LDStrDup(const char *const string); + void * LDRealloc(void *const buffer, const size_t bytes); + void * LDCalloc(const size_t nmemb, const size_t size); + char * LDStrNDup(const char *const str, const size_t n); + void LDSetMemoryRoutines(void *(*const newMalloc)(const size_t), + void (*const newFree)(void *const), + void *(*const newRealloc)(void *const, const size_t), + char *(*const newStrDup)(const char *const), + void *(*const newCalloc)(const size_t, const size_t), + char *(*const newStrNDup)(const char *const, const size_t)); + void LDGlobalInit(); + typedef enum { + LD_LOG_FATAL = 0, + LD_LOG_CRITICAL, + LD_LOG_ERROR, + LD_LOG_WARNING, + LD_LOG_INFO, + LD_LOG_DEBUG, + LD_LOG_TRACE + } LDLogLevel; + void LDi_log(const LDLogLevel level, const char *const format, ...); + void LDBasicLogger(const LDLogLevel level, const char *const text); + void LDConfigureGlobalLogger(const LDLogLevel level, + void (*logger)(const LDLogLevel level, const char *const text)); + const char * LDLogLevelToString(const LDLogLevel level); + enum LDEvalReason { + LD_UNKNOWN = 0, + LD_ERROR, + LD_OFF, + LD_PREREQUISITE_FAILED, + LD_TARGET_MATCH, + LD_RULE_MATCH, + LD_FALLTHROUGH + }; + enum LDEvalErrorKind { + LD_CLIENT_NOT_READY, + LD_NULL_KEY, + LD_STORE_ERROR, + LD_FLAG_NOT_FOUND, + LD_USER_NOT_SPECIFIED, + LD_MALFORMED_FLAG, + LD_WRONG_TYPE, + LD_OOM + }; + struct LDDetailsRule { + unsigned int ruleIndex; + char *id; + }; + struct LDDetails { + unsigned int variationIndex; + bool hasVariation; + enum LDEvalReason reason; + union { + enum LDEvalErrorKind errorKind; + char *prerequisiteKey; + struct LDDetailsRule rule; + } extra; + }; + void LDDetailsInit(struct LDDetails *const details); + void LDDetailsClear(struct LDDetails *const details); + const char * LDEvalReasonKindToString(const enum LDEvalReason kind); + const char * LDEvalErrorKindToString( + const enum LDEvalErrorKind kind); + struct LDJSON * LDReasonToJSON( + const struct LDDetails *const details); + bool LDBoolVariation(struct LDClient *const client, + struct LDUser *const user, const char *const key, const bool fallback, + struct LDDetails *const details); + int LDIntVariation(struct LDClient *const client, + struct LDUser *const user, const char *const key, const int fallback, + struct LDDetails *const details); + double LDDoubleVariation(struct LDClient *const client, + struct LDUser *const user, const char *const key, const double fallback, + struct LDDetails *const details); + char * LDStringVariation(struct LDClient *const client, + struct LDUser *const user, const char *const key, + const char* const fallback, struct LDDetails *const details); + struct LDJSON * LDJSONVariation(struct LDClient *const client, + struct LDUser *const user, const char *const key, + const struct LDJSON *const fallback, struct LDDetails *const details); + struct LDJSON * LDAllFlags(struct LDClient *const client, + struct LDUser *const user); +]] + +ld.so = ffi.load("ldserverapi") + +function applyWhenNotNil(subject, operation, value) + if value ~= nil and value ~= cjson.null then + operation(subject, value) + end +end + +function toLaunchDarklyJSON(x) + return ffi.gc(ld.so.LDJSONDeserialize(cjson.encode(x)), ld.so.LDJSONFree) +end + +function toLaunchDarklyJSONTransfer(x) + return ld.so.LDJSONDeserialize(cjson.encode(x)) +end + +function fromLaunchDarklyJSON(x) + local raw = ld.so.LDJSONSerialize(x) + local native = ffi.string(raw) + ld.so.LDFree(raw) + return cjson.decode(native) +end + +local makeConfig = function(fields) + local config = ld.so.LDConfigNew(fields["key"]) + + applyWhenNotNil(config, ld.so.LDConfigSetBaseURI, fields["baseURI"]) + applyWhenNotNil(config, ld.so.LDConfigSetStreamURI, fields["streamURI"]) + applyWhenNotNil(config, ld.so.LDConfigSetEventsURI, fields["eventsURI"]) + applyWhenNotNil(config, ld.so.LDConfigSetStream, fields["stream"]) + applyWhenNotNil(config, ld.so.LDConfigSetSendEvents, fields["sendEvents"]) + applyWhenNotNil(config, ld.so.LDConfigSetEventsCapacity, fields["eventsCapacity"]) + applyWhenNotNil(config, ld.so.LDConfigSetTimeout, fields["timeout"]) + applyWhenNotNil(config, ld.so.LDConfigSetFlushInterval, fields["flushInterval"]) + applyWhenNotNil(config, ld.so.LDConfigSetPollInterval, fields["pollInterval"]) + applyWhenNotNil(config, ld.so.LDConfigSetOffline, fields["offline"]) + applyWhenNotNil(config, ld.so.LDConfigSetAllAttributesPrivate, fields["allAttributesPrivate"]) + applyWhenNotNil(config, ld.so.LDConfigInlineUsersInEvents, fields["inlineUsersInEvents"]) + applyWhenNotNil(config, ld.so.LDConfigSetUserKeysCapacity, fields["userKeysCapacity"]) + applyWhenNotNil(config, ld.so.LDConfigSetUserKeysFlushInterval, fields["userKeysFlushInterval"]) + + local names = fields["privateAttributeNames"] + + if names ~= nil and names ~= cjson.null then + for _, v in ipairs(names) do + ld.so.LDConfigAddPrivateAttribute(config, v) + end + end + + return config +end + +ld.makeUser = function(fields) + local user = ffi.gc(ld.so.LDUserNew(fields["key"]), ld.so.LDUserFree) + + applyWhenNotNil(user, ld.so.LDUserSetAnonymous, fields["anonymous"]) + applyWhenNotNil(user, ld.so.LDUserSetIP, fields["ip"]) + applyWhenNotNil(user, ld.so.LDUserSetFirstName, fields["firstName"]) + applyWhenNotNil(user, ld.so.LDUserSetLastName, fields["lastName"]) + applyWhenNotNil(user, ld.so.LDUserSetEmail, fields["email"]) + applyWhenNotNil(user, ld.so.LDUserSetName, fields["name"]) + applyWhenNotNil(user, ld.so.LDUserSetAvatar, fields["avatar"]) + applyWhenNotNil(user, ld.so.LDUserSetCountry, fields["country"]) + applyWhenNotNil(user, ld.so.LDUserSetSecondary, fields["secondary"]) + + if fields["custom"] ~= nil then + ld.so.LDUserSetCustom(user, toLaunchDarklyJSONTransfer(fields["custom"])) + end + + local names = fields["privateAttributeNames"] + + if names ~= nil and names ~= cjson.null then + for _, v in ipairs(names) do + ngx.log(ngx.ERR, "value: " .. v) + ld.so.LDUserAddPrivateAttribute(user, v) + end + end + + return user +end + +ld.clientInit = function(config, timoutMilliseconds) + local interface = {} + + local client = ffi.gc(ld.so.LDClientInit(makeConfig(config), 1000), ld.so.LDClientClose) + + interface.isInitialized = function() + return ld.so.LDClientIsInitialized(client) + end + + interface.identify = function(user) + ld.so.LDClientIdentify(client, user) + end + + interface.isOffline = function() + ld.so.LDClientIsOffline(client) + end + + interface.flush = function() + ld.so.LDClientFlush(client) + end + + interface.track = function(key, user, data, metric) + local json = nil + + if data ~= nil then + json = toLaunchDarklyJSON(data) + end + + if metric ~= nil then + ld.so.LDClientTrackMetric(client, key, user, json, metric) + else + ld.so.LDClientTrack(client, key, user, json) + end + end + + interface.allFlags = function(user) + local x = ld.so.LDAllFlags(client, user) + if x ~= nil then + return fromLaunchDarklyJSON(x) + else + return nil + end + end + + interface.boolVariation = function(user, key, fallback) + return ld.so.LDBoolVariation(client, user, key, fallback, nil) + end + + interface.intVariation = function(user, key, fallback) + return ld.so.LDIntVariation(client, user, key, fallback, nil) + end + + interface.doubleVariation = function(user, key, fallback) + return ld.so.LDDoubleVariation(client, user, key, fallback, nil) + end + + interface.stringVariation = function(user, key, fallback) + local raw = ld.so.LDStringVariation(client, user, key, fallback, nil) + local native = ffi.string(raw) + ld.so.LDFree(raw) + return native + end + + interface.jsonVariation = function(user, key, fallback) + return fromLaunchDarklyJSON( + ld.so.LDJSONVariation(client, user, key, toLaunchDarklyJSON(fallback), nil) + ) + end + + return interface +end + +return ld From b41bd5a59314d6a337db02322048a096e3254870 Mon Sep 17 00:00:00 2001 From: hroederld Date: Thu, 27 Feb 2020 16:26:47 -0800 Subject: [PATCH 002/161] [ch66755] Add variationDetail variation handlers (#2) --- launchdarkly.lua | 64 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 3 deletions(-) diff --git a/launchdarkly.lua b/launchdarkly.lua index 6468fec..b8cdae1 100644 --- a/launchdarkly.lua +++ b/launchdarkly.lua @@ -235,6 +235,31 @@ function fromLaunchDarklyJSON(x) return cjson.decode(native) end +function convertDetails(cDetails, value) + local details = {} + local cReasonJSON = ld.so.LDReasonToJSON(cDetails) + details.reason = fromLaunchDarklyJSON(cReasonJSON) + + if cDetails.hasVariation then + details.variationIndex = cDetails.variationIndex + end + + details.value = value + return details +end + +function genericVariationDetail(client, user, key, fallback, variation, valueConverter) + local cDetails = ffi.new("struct LDDetails") + ld.so.LDDetailsInit(cDetails) + local value = variation(client, user, key, fallback, cDetails) + if valueConverter ~= nil then + value = valueConverter(value) + end + local details = convertDetails(cDetails, value) + ld.so.LDDetailsClear(cDetails) + return details +end + local makeConfig = function(fields) local config = ld.so.LDConfigNew(fields["key"]) @@ -341,14 +366,26 @@ ld.clientInit = function(config, timoutMilliseconds) return ld.so.LDBoolVariation(client, user, key, fallback, nil) end + interface.boolVariationDetail = function(user, key, fallback) + return genericVariationDetail(client, user, key, fallback, ld.so.LDBoolVariation, nil) + end + interface.intVariation = function(user, key, fallback) return ld.so.LDIntVariation(client, user, key, fallback, nil) end + interface.intVariationDetail = function(user, key, fallback) + return genericVariationDetail(client, user, key, fallback, ld.so.LDIntVariation, nil) + end + interface.doubleVariation = function(user, key, fallback) return ld.so.LDDoubleVariation(client, user, key, fallback, nil) end + interface.doubleVariationDetail = function(user, key, fallback) + return genericVariationDetail(client, user, key, fallback, ld.so.LDDoubleVariation, nil) + end + interface.stringVariation = function(user, key, fallback) local raw = ld.so.LDStringVariation(client, user, key, fallback, nil) local native = ffi.string(raw) @@ -356,10 +393,31 @@ ld.clientInit = function(config, timoutMilliseconds) return native end + interface.stringVariationDetail = function(user, key, fallback) + local valueConverter = function(raw) + local native = ffi.string(raw) + ld.so.LDFree(raw) + return native + end + + return genericVariationDetail(client, user, key, fallback, ld.so.LDStringVariation, valueConverter) + end + interface.jsonVariation = function(user, key, fallback) - return fromLaunchDarklyJSON( - ld.so.LDJSONVariation(client, user, key, toLaunchDarklyJSON(fallback), nil) - ) + local raw = ld.so.LDJSONVariation(client, user, key, toLaunchDarklyJSON(fallback), nil) + local native = fromLaunchDarklyJSON(raw) + ld.so.LDJSONFree(raw) + return native + end + + interface.jsonVariationDetail = function(user, key, fallback) + local valueConverter = function(raw) + local native = fromLaunchDarklyJSON(raw) + ld.so.LDJSONFree(raw) + return native + end + + return genericVariationDetail(client, user, key, toLaunchDarklyJSON(fallback), ld.so.LDJSONVariation, valueConverter) end return interface From 4a001eb2d92dce7ec88d526473dad09c69af3b35 Mon Sep 17 00:00:00 2001 From: hroederld Date: Mon, 9 Mar 2020 11:25:19 -0700 Subject: [PATCH 003/161] [ch68367] prefix functions with local --- launchdarkly.lua | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/launchdarkly.lua b/launchdarkly.lua index b8cdae1..34cbc55 100644 --- a/launchdarkly.lua +++ b/launchdarkly.lua @@ -214,28 +214,28 @@ ffi.cdef[[ ld.so = ffi.load("ldserverapi") -function applyWhenNotNil(subject, operation, value) +local function applyWhenNotNil(subject, operation, value) if value ~= nil and value ~= cjson.null then operation(subject, value) end end -function toLaunchDarklyJSON(x) +local function toLaunchDarklyJSON(x) return ffi.gc(ld.so.LDJSONDeserialize(cjson.encode(x)), ld.so.LDJSONFree) end -function toLaunchDarklyJSONTransfer(x) +local function toLaunchDarklyJSONTransfer(x) return ld.so.LDJSONDeserialize(cjson.encode(x)) end -function fromLaunchDarklyJSON(x) +local function fromLaunchDarklyJSON(x) local raw = ld.so.LDJSONSerialize(x) local native = ffi.string(raw) ld.so.LDFree(raw) return cjson.decode(native) end -function convertDetails(cDetails, value) +local function convertDetails(cDetails, value) local details = {} local cReasonJSON = ld.so.LDReasonToJSON(cDetails) details.reason = fromLaunchDarklyJSON(cReasonJSON) @@ -248,7 +248,7 @@ function convertDetails(cDetails, value) return details end -function genericVariationDetail(client, user, key, fallback, variation, valueConverter) +local function genericVariationDetail(client, user, key, fallback, variation, valueConverter) local cDetails = ffi.new("struct LDDetails") ld.so.LDDetailsInit(cDetails) local value = variation(client, user, key, fallback, cDetails) @@ -260,7 +260,7 @@ function genericVariationDetail(client, user, key, fallback, variation, valueCon return details end -local makeConfig = function(fields) +local function makeConfig(fields) local config = ld.so.LDConfigNew(fields["key"]) applyWhenNotNil(config, ld.so.LDConfigSetBaseURI, fields["baseURI"]) From c66ed759f6633d04d09dea94a91fab21647e5408 Mon Sep 17 00:00:00 2001 From: hroederld Date: Tue, 10 Mar 2020 15:04:29 -0700 Subject: [PATCH 004/161] [ch68610] Basic generated doc (#4) --- launchdarkly.lua | 328 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 262 insertions(+), 66 deletions(-) diff --git a/launchdarkly.lua b/launchdarkly.lua index 34cbc55..92ab778 100644 --- a/launchdarkly.lua +++ b/launchdarkly.lua @@ -1,8 +1,9 @@ +--- Server-side SDK for LaunchDarkly. +-- @module launchdarkly-server-sdk + local ffi = require("ffi") local cjson = require("cjson") -local ld = {} - ffi.cdef[[ struct LDJSON; typedef enum { @@ -212,7 +213,7 @@ ffi.cdef[[ struct LDUser *const user); ]] -ld.so = ffi.load("ldserverapi") +local so = ffi.load("ldserverapi") local function applyWhenNotNil(subject, operation, value) if value ~= nil and value ~= cjson.null then @@ -221,23 +222,40 @@ local function applyWhenNotNil(subject, operation, value) end local function toLaunchDarklyJSON(x) - return ffi.gc(ld.so.LDJSONDeserialize(cjson.encode(x)), ld.so.LDJSONFree) + return ffi.gc(so.LDJSONDeserialize(cjson.encode(x)), so.LDJSONFree) end local function toLaunchDarklyJSONTransfer(x) - return ld.so.LDJSONDeserialize(cjson.encode(x)) + return so.LDJSONDeserialize(cjson.encode(x)) end local function fromLaunchDarklyJSON(x) - local raw = ld.so.LDJSONSerialize(x) + local raw = so.LDJSONSerialize(x) local native = ffi.string(raw) - ld.so.LDFree(raw) + so.LDFree(raw) return cjson.decode(native) end +--- Details associated with an evaluation +-- @name Details +-- @class table +-- @tfield[opt] int variationIndex The index of the returned value within the +-- flag's list of variations. +-- @field value The resulting value of an evaluation +-- @tfield table reason The reason a specific value was returned +-- @tfield string reason.kind The kind of reason +-- @tfield[opt] string reason.errorKind If the kind is LD_ERROR, this contains +-- the error string. +-- @tfield[opt] string reason.ruleId If the kind is LD_RULE_MATCH this contains +-- the id of the rule. +-- @tfield[opt] int reason.ruleIndex If the kind is LD_RULE_MATCH this contains +-- the index of the rule. +-- @tfield[opt] string reason.prerequisiteKey If the kind is +-- LD_PREREQUISITE_FAILED this contains the key of the failed prerequisite. + local function convertDetails(cDetails, value) local details = {} - local cReasonJSON = ld.so.LDReasonToJSON(cDetails) + local cReasonJSON = so.LDReasonToJSON(cDetails) details.reason = fromLaunchDarklyJSON(cReasonJSON) if cDetails.hasVariation then @@ -250,95 +268,191 @@ end local function genericVariationDetail(client, user, key, fallback, variation, valueConverter) local cDetails = ffi.new("struct LDDetails") - ld.so.LDDetailsInit(cDetails) + so.LDDetailsInit(cDetails) local value = variation(client, user, key, fallback, cDetails) if valueConverter ~= nil then value = valueConverter(value) end local details = convertDetails(cDetails, value) - ld.so.LDDetailsClear(cDetails) + so.LDDetailsClear(cDetails) return details end +--- make a config local function makeConfig(fields) - local config = ld.so.LDConfigNew(fields["key"]) - - applyWhenNotNil(config, ld.so.LDConfigSetBaseURI, fields["baseURI"]) - applyWhenNotNil(config, ld.so.LDConfigSetStreamURI, fields["streamURI"]) - applyWhenNotNil(config, ld.so.LDConfigSetEventsURI, fields["eventsURI"]) - applyWhenNotNil(config, ld.so.LDConfigSetStream, fields["stream"]) - applyWhenNotNil(config, ld.so.LDConfigSetSendEvents, fields["sendEvents"]) - applyWhenNotNil(config, ld.so.LDConfigSetEventsCapacity, fields["eventsCapacity"]) - applyWhenNotNil(config, ld.so.LDConfigSetTimeout, fields["timeout"]) - applyWhenNotNil(config, ld.so.LDConfigSetFlushInterval, fields["flushInterval"]) - applyWhenNotNil(config, ld.so.LDConfigSetPollInterval, fields["pollInterval"]) - applyWhenNotNil(config, ld.so.LDConfigSetOffline, fields["offline"]) - applyWhenNotNil(config, ld.so.LDConfigSetAllAttributesPrivate, fields["allAttributesPrivate"]) - applyWhenNotNil(config, ld.so.LDConfigInlineUsersInEvents, fields["inlineUsersInEvents"]) - applyWhenNotNil(config, ld.so.LDConfigSetUserKeysCapacity, fields["userKeysCapacity"]) - applyWhenNotNil(config, ld.so.LDConfigSetUserKeysFlushInterval, fields["userKeysFlushInterval"]) + local config = so.LDConfigNew(fields["key"]) + + applyWhenNotNil(config, so.LDConfigSetBaseURI, fields["baseURI"]) + applyWhenNotNil(config, so.LDConfigSetStreamURI, fields["streamURI"]) + applyWhenNotNil(config, so.LDConfigSetEventsURI, fields["eventsURI"]) + applyWhenNotNil(config, so.LDConfigSetStream, fields["stream"]) + applyWhenNotNil(config, so.LDConfigSetSendEvents, fields["sendEvents"]) + applyWhenNotNil(config, so.LDConfigSetEventsCapacity, fields["eventsCapacity"]) + applyWhenNotNil(config, so.LDConfigSetTimeout, fields["timeout"]) + applyWhenNotNil(config, so.LDConfigSetFlushInterval, fields["flushInterval"]) + applyWhenNotNil(config, so.LDConfigSetPollInterval, fields["pollInterval"]) + applyWhenNotNil(config, so.LDConfigSetOffline, fields["offline"]) + applyWhenNotNil(config, so.LDConfigSetAllAttributesPrivate, fields["allAttributesPrivate"]) + applyWhenNotNil(config, so.LDConfigInlineUsersInEvents, fields["inlineUsersInEvents"]) + applyWhenNotNil(config, so.LDConfigSetUserKeysCapacity, fields["userKeysCapacity"]) + applyWhenNotNil(config, so.LDConfigSetUserKeysFlushInterval, fields["userKeysFlushInterval"]) local names = fields["privateAttributeNames"] if names ~= nil and names ~= cjson.null then for _, v in ipairs(names) do - ld.so.LDConfigAddPrivateAttribute(config, v) + so.LDConfigAddPrivateAttribute(config, v) end end return config end -ld.makeUser = function(fields) - local user = ffi.gc(ld.so.LDUserNew(fields["key"]), ld.so.LDUserFree) - - applyWhenNotNil(user, ld.so.LDUserSetAnonymous, fields["anonymous"]) - applyWhenNotNil(user, ld.so.LDUserSetIP, fields["ip"]) - applyWhenNotNil(user, ld.so.LDUserSetFirstName, fields["firstName"]) - applyWhenNotNil(user, ld.so.LDUserSetLastName, fields["lastName"]) - applyWhenNotNil(user, ld.so.LDUserSetEmail, fields["email"]) - applyWhenNotNil(user, ld.so.LDUserSetName, fields["name"]) - applyWhenNotNil(user, ld.so.LDUserSetAvatar, fields["avatar"]) - applyWhenNotNil(user, ld.so.LDUserSetCountry, fields["country"]) - applyWhenNotNil(user, ld.so.LDUserSetSecondary, fields["secondary"]) +--- Create a new opaque user object. +-- @tparam table fields list of user fields. +-- @tparam string fields.key The user's key +-- @tparam[opt] boolean fields.anonymous Mark the user as anonymous +-- @tparam[opt] string fields.ip Set the user's IP +-- @tparam[opt] string fields.firstName Set the user's first name +-- @tparam[opt] string fields.lastName Set the user's last name +-- @tparam[opt] string fields.email Set the user's email +-- @tparam[opt] string fields.name Set the user's name +-- @tparam[opt] string fields.avatar Set the user's avatar +-- @tparam[opt] string fields.country Set the user's country +-- @tparam[opt] string fields.secondary Set the user's secondary key +-- @tparam[opt] table fields.privateAttributeNames A list of attributes to +-- redact +-- @tparam[opt] table fields.custom Set the user's custom JSON +-- @return an opaque user object +local function makeUser(fields) + local user = ffi.gc(so.LDUserNew(fields["key"]), so.LDUserFree) + + applyWhenNotNil(user, so.LDUserSetAnonymous, fields["anonymous"]) + applyWhenNotNil(user, so.LDUserSetIP, fields["ip"]) + applyWhenNotNil(user, so.LDUserSetFirstName, fields["firstName"]) + applyWhenNotNil(user, so.LDUserSetLastName, fields["lastName"]) + applyWhenNotNil(user, so.LDUserSetEmail, fields["email"]) + applyWhenNotNil(user, so.LDUserSetName, fields["name"]) + applyWhenNotNil(user, so.LDUserSetAvatar, fields["avatar"]) + applyWhenNotNil(user, so.LDUserSetCountry, fields["country"]) + applyWhenNotNil(user, so.LDUserSetSecondary, fields["secondary"]) if fields["custom"] ~= nil then - ld.so.LDUserSetCustom(user, toLaunchDarklyJSONTransfer(fields["custom"])) + so.LDUserSetCustom(user, toLaunchDarklyJSONTransfer(fields["custom"])) end local names = fields["privateAttributeNames"] if names ~= nil and names ~= cjson.null then for _, v in ipairs(names) do - ngx.log(ngx.ERR, "value: " .. v) - ld.so.LDUserAddPrivateAttribute(user, v) + so.LDUserAddPrivateAttribute(user, v) end end return user end -ld.clientInit = function(config, timoutMilliseconds) +--- Initialize a new client, and connect to LaunchDarkly. +-- @tparam table config list of configuration options +-- @tparam string config.key Environment SDK key +-- @tparam[opt] string config.baseURI Set the base URI for connecting to +-- LaunchDarkly. You probably don't need to set this unless instructed by +-- LaunchDarkly. +-- @tparam[opt] string config.streamURI Set the streaming URI for connecting to +-- LaunchDarkly. You probably don't need to set this unless instructed by +-- LaunchDarkly. +-- @tparam[opt] string config.eventsURI Set the events URI for connecting to +-- LaunchDarkly. You probably don't need to set this unless instructed by +-- LaunchDarkly. +-- @tparam[opt] boolean config.stream Enables or disables real-time streaming +-- flag updates. When set to false, an efficient caching polling mechanism is +-- used. We do not recommend disabling streaming unless you have been instructed +-- to do so by LaunchDarkly support. Defaults to true. +-- @tparam[opt] string config.sendEvents Sets whether to send analytics events +-- back to LaunchDarkly. By default, the client will send events. This differs +-- from Offline in that it only affects sending events, not streaming or +-- polling. +-- @tparam[opt] int config.eventsCapacity The capacity of the events buffer. +-- The client buffers up to this many events in memory before flushing. If the +-- capacity is exceeded before the buffer is flushed, events will be discarded. +-- @tparam[opt] int config.timeout The connection timeout to use when making +-- requests to LaunchDarkly. +-- @tparam[opt] int config.flushInterval he time between flushes of the event +-- buffer. Decreasing the flush interval means that the event buffer is less +-- likely to reach capacity. +-- @tparam[opt] int config.pollInterval The polling interval +-- (when streaming is disabled) in milliseconds. +-- @tparam[opt] boolean config.offline Sets whether this client is offline. +-- An offline client will not make any network connections to LaunchDarkly, +-- and will return default values for all feature flags. +-- @tparam[opt] boolean config.allAttributesPrivate Sets whether or not all user +-- attributes (other than the key) should be hidden from LaunchDarkly. If this +-- is true, all user attribute values will be private, not just the attributes +-- specified in PrivateAttributeNames. +-- @tparam[opt] boolean config.inlineUsersInEvents Set to true if you need to +-- see the full user details in every analytics event. +-- @tparam[opt] int config.userKeysCapacity The number of user keys that the +-- event processor can remember at an one time, so that duplicate user details +-- will not be sent in analytics. +-- @tparam[opt] int config.userKeysFlushInterval The interval at which the event +-- processor will reset its set of known user keys, in milliseconds. +-- @tparam[opt] table config.privateAttributeNames Marks a set of user attribute +-- names private. Any users sent to LaunchDarkly with this configuration active +-- will have attributes with these names removed. +-- @tparam int timeoutMilliseconds How long to wait for flags to download. +-- If the timeout is reached a non fully initialized client will be returned. +-- @return A fresh client. +local function clientInit(config, timeoutMilliseconds) local interface = {} - local client = ffi.gc(ld.so.LDClientInit(makeConfig(config), 1000), ld.so.LDClientClose) + --- An opaque client object + -- @type Client + + local client = ffi.gc(so.LDClientInit(makeConfig(config), 1000), so.LDClientClose) + --- Check if a client has been fully initialized. This may be useful if the + -- initialization timeout was reached. + -- @class function + -- @name isInitialized + -- @treturn boolean true if fully initialized interface.isInitialized = function() - return ld.so.LDClientIsInitialized(client) + return so.LDClientIsInitialized(client) end + --- Generates an identify event for a user. + -- @class function + -- @name identify + -- @tparam user user An opaque user object from @{makeUser} + -- @treturn nil interface.identify = function(user) - ld.so.LDClientIdentify(client, user) + so.LDClientIdentify(client, user) end + --- Whether the LaunchDarkly client is in offline mode. + -- @class function + -- @name isOffline + -- @treturn boolean true if offline interface.isOffline = function() - ld.so.LDClientIsOffline(client) + so.LDClientIsOffline(client) end + --- Immediately flushes queued events. + -- @class function + -- @name flush + -- @treturn nil interface.flush = function() - ld.so.LDClientFlush(client) + so.LDClientFlush(client) end + --- Reports that a user has performed an event. Custom data, and a metric + -- can be attached to the event as JSON. + -- @class function + -- @name track + -- @tparam string key The name of the event + -- @tparam user user An opaque user object from @{makeUser} + -- @tparam[opt] table data A value to be associated with an event + -- @tparam[optchain] number metric A value to be associated with an event + -- @treturn nil interface.track = function(key, user, data, metric) local json = nil @@ -347,14 +461,20 @@ ld.clientInit = function(config, timoutMilliseconds) end if metric ~= nil then - ld.so.LDClientTrackMetric(client, key, user, json, metric) + so.LDClientTrackMetric(client, key, user, json, metric) else - ld.so.LDClientTrack(client, key, user, json) + so.LDClientTrack(client, key, user, json) end end + --- Returns a map from feature flag keys to values for a given user. + -- This does not send analytics events back to LaunchDarkly. + -- @class function + -- @name allFlags + -- @tparam user user An opaque user object from @{makeUser} + -- @treturn table interface.allFlags = function(user) - local x = ld.so.LDAllFlags(client, user) + local x = so.LDAllFlags(client, user) if x ~= nil then return fromLaunchDarklyJSON(x) else @@ -362,65 +482,141 @@ ld.clientInit = function(config, timoutMilliseconds) end end + --- Evaluate a boolean flag + -- @class function + -- @name boolVariation + -- @tparam user user An opaque user object from @{makeUser} + -- @tparam string key The key of the flag to evaluate. + -- @tparam boolean fallback The value to return on error + -- @treturn boolean The evaluation result, or the fallback value interface.boolVariation = function(user, key, fallback) - return ld.so.LDBoolVariation(client, user, key, fallback, nil) + return so.LDBoolVariation(client, user, key, fallback, nil) end + --- Evaluate a boolean flag and return an explanation + -- @class function + -- @name boolVariationDetail + -- @tparam user user An opaque user object from @{makeUser} + -- @tparam string key The key of the flag to evaluate. + -- @tparam boolean fallback The value to return on error + -- @treturn table The evaluation explanation interface.boolVariationDetail = function(user, key, fallback) - return genericVariationDetail(client, user, key, fallback, ld.so.LDBoolVariation, nil) + return genericVariationDetail(client, user, key, fallback, so.LDBoolVariation, nil) end + --- Evaluate an integer flag + -- @class function + -- @name intVariation + -- @tparam user user An opaque user object from @{makeUser} + -- @tparam string key The key of the flag to evaluate. + -- @tparam int fallback The value to return on error + -- @treturn int The evaluation result, or the fallback value interface.intVariation = function(user, key, fallback) - return ld.so.LDIntVariation(client, user, key, fallback, nil) + return so.LDIntVariation(client, user, key, fallback, nil) end + --- Evaluate an integer flag and return an explanation + -- @class function + -- @name intVariationDetail + -- @tparam user user An opaque user object from @{makeUser} + -- @tparam string key The key of the flag to evaluate. + -- @tparam int fallback The value to return on error + -- @treturn table The evaluation explanation interface.intVariationDetail = function(user, key, fallback) - return genericVariationDetail(client, user, key, fallback, ld.so.LDIntVariation, nil) + return genericVariationDetail(client, user, key, fallback, so.LDIntVariation, nil) end + --- Evaluate a double flag + -- @class function + -- @name doubleVariation + -- @tparam user user An opaque user object from @{makeUser} + -- @tparam string key The key of the flag to evaluate. + -- @tparam number fallback The value to return on error + -- @treturn double The evaluation result, or the fallback value interface.doubleVariation = function(user, key, fallback) - return ld.so.LDDoubleVariation(client, user, key, fallback, nil) + return so.LDDoubleVariation(client, user, key, fallback, nil) end + --- Evaluate a double flag and return an explanation + -- @class function + -- @name doubleVariationDetail + -- @tparam user user An opaque user object from @{makeUser} + -- @tparam string key The key of the flag to evaluate. + -- @tparam number fallback The value to return on error + -- @treturn table The evaluation explanation interface.doubleVariationDetail = function(user, key, fallback) - return genericVariationDetail(client, user, key, fallback, ld.so.LDDoubleVariation, nil) + return genericVariationDetail(client, user, key, fallback, so.LDDoubleVariation, nil) end + --- Evaluate a string flag + -- @class function + -- @name stringVariation + -- @tparam user user An opaque user object from @{makeUser} + -- @tparam string key The key of the flag to evaluate. + -- @tparam string fallback The value to return on error + -- @treturn string The evaluation result, or the fallback value interface.stringVariation = function(user, key, fallback) - local raw = ld.so.LDStringVariation(client, user, key, fallback, nil) + local raw = so.LDStringVariation(client, user, key, fallback, nil) local native = ffi.string(raw) - ld.so.LDFree(raw) + so.LDFree(raw) return native end + --- Evaluate a string flag and return an explanation + -- @class function + -- @name stringVariationDetail + -- @tparam user user An opaque user object from @{makeUser} + -- @tparam string key The key of the flag to evaluate. + -- @tparam string fallback The value to return on error + -- @treturn table The evaluation explanation interface.stringVariationDetail = function(user, key, fallback) local valueConverter = function(raw) local native = ffi.string(raw) - ld.so.LDFree(raw) + so.LDFree(raw) return native end - return genericVariationDetail(client, user, key, fallback, ld.so.LDStringVariation, valueConverter) + return genericVariationDetail(client, user, key, fallback, so.LDStringVariation, valueConverter) end + --- Evaluate a json flag + -- @class function + -- @name jsonVariation + -- @tparam user user An opaque user object from @{makeUser} + -- @tparam string key The key of the flag to evaluate. + -- @tparam table fallback The value to return on error + -- @treturn table The evaluation result, or the fallback value interface.jsonVariation = function(user, key, fallback) - local raw = ld.so.LDJSONVariation(client, user, key, toLaunchDarklyJSON(fallback), nil) + local raw = so.LDJSONVariation(client, user, key, toLaunchDarklyJSON(fallback), nil) local native = fromLaunchDarklyJSON(raw) - ld.so.LDJSONFree(raw) + so.LDJSONFree(raw) return native end + --- Evaluate a json flag and return an explanation + -- @class function + -- @name jsonVariationDetail + -- @tparam user user An opaque user object from @{makeUser} + -- @tparam string key The key of the flag to evaluate. + -- @tparam table fallback The value to return on error + -- @treturn table The evaluation explanation interface.jsonVariationDetail = function(user, key, fallback) local valueConverter = function(raw) local native = fromLaunchDarklyJSON(raw) - ld.so.LDJSONFree(raw) + so.LDJSONFree(raw) return native end - return genericVariationDetail(client, user, key, toLaunchDarklyJSON(fallback), ld.so.LDJSONVariation, valueConverter) + return genericVariationDetail(client, user, key, toLaunchDarklyJSON(fallback), so.LDJSONVariation, valueConverter) end + --- @type end + return interface end -return ld +--- @export +return { + makeUser = makeUser, + clientInit = clientInit +} From e69359c5e1769376528a1ef95c3911b95fdb4f2d Mon Sep 17 00:00:00 2001 From: hroederld Date: Wed, 18 Mar 2020 19:49:46 -0700 Subject: [PATCH 005/161] [ch70062] rename to launchdarkly-server-sdk.lua --- launchdarkly.lua => launchdarkly-server-sdk.lua | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename launchdarkly.lua => launchdarkly-server-sdk.lua (100%) diff --git a/launchdarkly.lua b/launchdarkly-server-sdk.lua similarity index 100% rename from launchdarkly.lua rename to launchdarkly-server-sdk.lua From 24b948718a4147837aad984a40b5f8e706bb2d0e Mon Sep 17 00:00:00 2001 From: hroederld Date: Tue, 24 Mar 2020 14:51:23 -0700 Subject: [PATCH 006/161] [ch70700] prepare 1.0.0-beta.1 --- CHANGELOG.md | 7 +++++++ LICENSE | 13 +++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 LICENSE diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..830fc1a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# Change log + +All notable changes to the LaunchDarkly Lua Server-side SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). + +## [1.0.0-beta.1] - 2020-03-24 + +Initial beta release. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f711cb7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright 2020 Catamorphic, Co. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. From 6ed903a8666e0f9f2187a687b7e936cfaf6c55ba Mon Sep 17 00:00:00 2001 From: Ben Woskow <48036130+bwoskow-ld@users.noreply.github.com> Date: Tue, 24 Mar 2020 14:52:08 -0700 Subject: [PATCH 007/161] Update CONTRIBUTING.md (#8) --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cb41d17..51ae91c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ -Contributing to the LaunchDarkly Server SDK for Lua +Contributing to the LaunchDarkly Server-Side SDK for Lua ================================================ LaunchDarkly has published an [SDK contributor's guide](https://docs.launchdarkly.com/docs/sdk-contributors-guide) that provides a detailed explanation of how our SDKs work. See below for additional information on how to contribute to this SDK. From 17a35ef00027549577e7fdb15b09121ca569b960 Mon Sep 17 00:00:00 2001 From: Ben Woskow <48036130+bwoskow-ld@users.noreply.github.com> Date: Tue, 24 Mar 2020 14:52:14 -0700 Subject: [PATCH 008/161] Update README.md (#7) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fb7f090..4d2483a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -LaunchDarkly server SDK for Lua +LaunchDarkly Server-Side SDK for Lua =========================== [![Circle CI](https://circleci.com/gh/launchdarkly/lua-server-sdk.svg?style=shield)](https://circleci.com/gh/launchdarkly/lua-server-sdk) From 791dd2e36c2ed8392210c86f7f52850cea3069c7 Mon Sep 17 00:00:00 2001 From: hroederld Date: Wed, 25 Mar 2020 12:51:51 -0700 Subject: [PATCH 009/161] remove circle badge (#9) --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4d2483a..6585ef9 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ LaunchDarkly Server-Side SDK for Lua =========================== -[![Circle CI](https://circleci.com/gh/launchdarkly/lua-server-sdk.svg?style=shield)](https://circleci.com/gh/launchdarkly/lua-server-sdk) - *This version of the SDK is a **beta** version and should not be considered ready for production use while this message is visible.* LaunchDarkly overview @@ -14,12 +12,12 @@ LaunchDarkly overview Getting started ----------- -Refer to the [SDK documentation](https://docs.launchdarkly.com/docs/lua-server-reference#section-getting-started) for instructions on getting started with using the SDK. +Refer to the [SDK documentation](https://docs.launchdarkly.com/sdk/server-side/lua#getting-started) for instructions on getting started with using the SDK. Learn more ----------- -Check out our [documentation](https://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. You can also head straight to the [complete reference guide for this SDK](https://docs.launchdarkly.com/docs/lua-server-reference). +Check out our [documentation](https://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. You can also head straight to the [complete reference guide for this SDK](https://docs.launchdarkly.com/sdk/server-side/lua). Testing ------- From b1f585cf378d8a0c7e155e7950690baff60d1707 Mon Sep 17 00:00:00 2001 From: hroederld Date: Wed, 1 Apr 2020 10:32:36 -0700 Subject: [PATCH 010/161] [ch70699] releaser support (#11) --- .circleci/config.yml | 20 ++++++++++++++++++++ .ldrelease/config.yml | 14 ++++++++++++++ .ldrelease/linux-build-docs.sh | 13 +++++++++++++ .ldrelease/linux-prepare.sh | 5 +++++ 4 files changed, 52 insertions(+) create mode 100644 .circleci/config.yml create mode 100644 .ldrelease/config.yml create mode 100755 .ldrelease/linux-build-docs.sh create mode 100755 .ldrelease/linux-prepare.sh diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..590166b --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,20 @@ +version: 2.1 + +workflows: + version: 2 + all: + jobs: + - build-doc-linux + +jobs: + build-doc-linux: + docker: + - image: ubuntu:18.04 + steps: + - checkout + - run: + name: Prepare + command: ./.ldrelease/linux-prepare.sh + - run: + name: Build Doc + command: ./.ldrelease/linux-build-docs.sh diff --git a/.ldrelease/config.yml b/.ldrelease/config.yml new file mode 100644 index 0000000..32e5ac5 --- /dev/null +++ b/.ldrelease/config.yml @@ -0,0 +1,14 @@ +repo: + public: lua-server-sdk + private: lua-server-sdk-private + +circleci: + linux: + image: ubuntu:18.04 + +documentation: + title: LaunchDarkly Server-Side SDK for Lua + githubPages: true + +sdk: + displayName: "Lua (server-side)" diff --git a/.ldrelease/linux-build-docs.sh b/.ldrelease/linux-build-docs.sh new file mode 100755 index 0000000..91557bb --- /dev/null +++ b/.ldrelease/linux-build-docs.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -e + +# This only runs in the Linux build, since the docs are the same for all platforms. + +PROJECT_DIR=$(pwd) + +ldoc launchdarkly-server-sdk.lua + +mkdir -p $PROJECT_DIR/artifacts +cd $PROJECT_DIR/doc +zip -r $PROJECT_DIR/artifacts/docs.zip * diff --git a/.ldrelease/linux-prepare.sh b/.ldrelease/linux-prepare.sh new file mode 100755 index 0000000..d3d8578 --- /dev/null +++ b/.ldrelease/linux-prepare.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +set -e + +apt-get update -y && apt-get install -y lua-ldoc zip ca-certificates From 3ab1ee2752db040d967d2a768ffaec23a12d113e Mon Sep 17 00:00:00 2001 From: hroederld Date: Thu, 16 Apr 2020 18:38:51 -0700 Subject: [PATCH 011/161] [ch73979] document version constraints (#12) --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 6585ef9..a5fe5b4 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,12 @@ LaunchDarkly overview [![Twitter Follow](https://img.shields.io/twitter/follow/launchdarkly.svg?style=social&label=Follow&maxAge=2592000)](https://twitter.com/intent/follow?screen_name=launchdarkly) + +Supported Lua versions +----------- + +This version of the LaunchDarkly SDK is compatible with the Lua 5.1 interpreter, and LuaJIT. Lua 5.3 is not supported due to FFI constraints. + Getting started ----------- From f2ed7f178b60447980c4666453dad7b2819f5f3e Mon Sep 17 00:00:00 2001 From: hroederld Date: Tue, 12 May 2020 08:41:59 -0700 Subject: [PATCH 012/161] [ch76282] add wrapper meta config (#13) --- .ldrelease/update-version.sh | 5 +++++ launchdarkly-server-sdk.lua | 6 ++++++ 2 files changed, 11 insertions(+) create mode 100755 .ldrelease/update-version.sh diff --git a/.ldrelease/update-version.sh b/.ldrelease/update-version.sh new file mode 100755 index 0000000..64d347a --- /dev/null +++ b/.ldrelease/update-version.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +set -e + +sed -i "s/local SDKVersion =.*/local SDKVersion = \"${LD_RELEASE_VERSION}\"/" 'launchdarkly-server-sdk.lua' diff --git a/launchdarkly-server-sdk.lua b/launchdarkly-server-sdk.lua index 92ab778..15d8bfa 100644 --- a/launchdarkly-server-sdk.lua +++ b/launchdarkly-server-sdk.lua @@ -91,6 +91,8 @@ ffi.cdef[[ const char *const attribute); void LDConfigSetFeatureStoreBackend(struct LDConfig *const config, struct LDStoreInterface *const backend); + bool LDConfigSetWrapperInfo(struct LDConfig *const config, + const char *const wrapperName, const char *const wrapperVersion); struct LDUser * LDUserNew(const char *const userkey); void LDUserFree(struct LDUser *const user); void LDUserSetAnonymous(struct LDUser *const user, const bool anon); @@ -213,6 +215,8 @@ ffi.cdef[[ struct LDUser *const user); ]] +local SDKVersion = "1.0.0-beta.1" + local so = ffi.load("ldserverapi") local function applyWhenNotNil(subject, operation, value) @@ -297,6 +301,8 @@ local function makeConfig(fields) applyWhenNotNil(config, so.LDConfigSetUserKeysCapacity, fields["userKeysCapacity"]) applyWhenNotNil(config, so.LDConfigSetUserKeysFlushInterval, fields["userKeysFlushInterval"]) + so.LDConfigSetWrapperInfo(config, "lua-server-sdk", SDKVersion) + local names = fields["privateAttributeNames"] if names ~= nil and names ~= cjson.null then From 57517c18a4dedc530712a09ef04a7cd362240404 Mon Sep 17 00:00:00 2001 From: Harpo Roeder Date: Tue, 12 May 2020 14:11:44 -0700 Subject: [PATCH 013/161] prepare 1.0.0-beta.2 (content approved in previous failed release PR) --- CHANGELOG.md | 5 +++++ launchdarkly-server-sdk.lua | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 830fc1a..9331c0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to the LaunchDarkly Lua Server-side SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [1.0.0-beta.2] - 2020-05-12 + +### Changed: +- Updates the configuration object to include wrapper name and version. + ## [1.0.0-beta.1] - 2020-03-24 Initial beta release. diff --git a/launchdarkly-server-sdk.lua b/launchdarkly-server-sdk.lua index 15d8bfa..bb72c73 100644 --- a/launchdarkly-server-sdk.lua +++ b/launchdarkly-server-sdk.lua @@ -215,7 +215,7 @@ ffi.cdef[[ struct LDUser *const user); ]] -local SDKVersion = "1.0.0-beta.1" +local SDKVersion = "1.0.0-beta.2" local so = ffi.load("ldserverapi") From bf8fd31ff12194855769dbc8e91f3b3ab7d24f64 Mon Sep 17 00:00:00 2001 From: hroederld Date: Tue, 23 Jun 2020 13:35:45 -0700 Subject: [PATCH 014/161] [ch80640] logging handler (#14) --- launchdarkly-server-sdk.lua | 40 +++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/launchdarkly-server-sdk.lua b/launchdarkly-server-sdk.lua index bb72c73..06c4790 100644 --- a/launchdarkly-server-sdk.lua +++ b/launchdarkly-server-sdk.lua @@ -282,6 +282,41 @@ local function genericVariationDetail(client, user, key, fallback, variation, va return details end +local function stringToLogLevel(level) + local translation = { + ["FATAL"] = so.LD_LOG_FATAL, + ["CRITICAL"] = so.LD_LOG_CRITICAL, + ["ERROR"] = so.LD_LOG_ERROR, + ["WARNING"] = so.LD_LOG_WARNING, + ["INFO"] = so.LD_LOG_INFO, + ["DEBUG"] = so.LD_LOG_DEBUG, + ["TRACE"] = so.LD_LOG_TRACE + } + + local lookup = translation[level] + + if lookup == nil then + return so.LD_LOG_INFO + else + return lookup + end +end + +--- Set the global logger for all SDK operations. This function is not thread +-- safe, and if used should be done so before other operations. The default +-- log level is "INFO". +-- @tparam string logLevel The level to at. Available options are: +-- "FATAL", "CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "TRACE". +-- @tparam function cb The logging handler. Callback must be of the form +-- "function (logLevel, logLine) ... end". +local function registerLogger(logLevel, cb) + so.LDConfigureGlobalLogger(stringToLogLevel(logLevel), + function(logLevel, line) + cb(ffi.string(so.LDLogLevelToString(logLevel)), ffi.string(line)) + end + ) +end + --- make a config local function makeConfig(fields) local config = so.LDConfigNew(fields["key"]) @@ -623,6 +658,7 @@ end --- @export return { - makeUser = makeUser, - clientInit = clientInit + registerLogger = registerLogger, + makeUser = makeUser, + clientInit = clientInit } From 77e52a1a1bb5e73a7ce2cbcc497944e41da50370 Mon Sep 17 00:00:00 2001 From: hroederld Date: Tue, 23 Jun 2020 15:32:01 -0700 Subject: [PATCH 015/161] [ch80816] basic unit tests (#15) --- .circleci/config.yml | 10 +- .ldrelease/linux-prepare.sh | 3 +- attribution/luaunit.txt | 12 + luaunit.lua | 3263 +++++++++++++++++++++++++++++++++++ scripts/fetch-linux.sh | 8 + test.lua | 39 + 6 files changed, 3332 insertions(+), 3 deletions(-) create mode 100644 attribution/luaunit.txt create mode 100644 luaunit.lua create mode 100755 scripts/fetch-linux.sh create mode 100644 test.lua diff --git a/.circleci/config.yml b/.circleci/config.yml index 590166b..da41be4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,10 +4,10 @@ workflows: version: 2 all: jobs: - - build-doc-linux + - build-test-linux jobs: - build-doc-linux: + build-test-linux: docker: - image: ubuntu:18.04 steps: @@ -18,3 +18,9 @@ jobs: - run: name: Build Doc command: ./.ldrelease/linux-build-docs.sh + - run: + name: Fetch c-server-sdk + command: ./scripts/fetch-linux.sh + - run: + name: Run tests + command: LD_LIBRARY_PATH=./lib luajit test.lua diff --git a/.ldrelease/linux-prepare.sh b/.ldrelease/linux-prepare.sh index d3d8578..b9e7621 100755 --- a/.ldrelease/linux-prepare.sh +++ b/.ldrelease/linux-prepare.sh @@ -2,4 +2,5 @@ set -e -apt-get update -y && apt-get install -y lua-ldoc zip ca-certificates +apt-get update -y && apt-get install -y luajit lua-ldoc zip ca-certificates \ + curl zip lua-cjson libpcre3 libcurl4-openssl-dev diff --git a/attribution/luaunit.txt b/attribution/luaunit.txt new file mode 100644 index 0000000..6f414b1 --- /dev/null +++ b/attribution/luaunit.txt @@ -0,0 +1,12 @@ +This software is distributed under the BSD License. + +Copyright (c) 2005-2018, Philippe Fremy + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/luaunit.lua b/luaunit.lua new file mode 100644 index 0000000..6753f13 --- /dev/null +++ b/luaunit.lua @@ -0,0 +1,3263 @@ +--[[ + luaunit.lua + +Description: A unit testing framework +Homepage: https://github.com/bluebird75/luaunit +Development by Philippe Fremy +Based on initial work of Ryu, Gwang (http://www.gpgstudy.com/gpgiki/LuaUnit) +License: BSD License, see LICENSE.txt +]]-- + +require("math") +local M={} + +-- private exported functions (for testing) +M.private = {} + +M.VERSION='3.4-dev' +M._VERSION=M.VERSION -- For LuaUnit v2 compatibility + +-- a version which distinguish between regular Lua and LuaJit +M._LUAVERSION = (jit and jit.version) or _VERSION + +--[[ Some people like assertEquals( actual, expected ) and some people prefer +assertEquals( expected, actual ). +]]-- +M.ORDER_ACTUAL_EXPECTED = true +M.PRINT_TABLE_REF_IN_ERROR_MSG = false +M.LINE_LENGTH = 80 +M.TABLE_DIFF_ANALYSIS_THRESHOLD = 10 -- display deep analysis for more than 10 items +M.LIST_DIFF_ANALYSIS_THRESHOLD = 10 -- display deep analysis for more than 10 items + +--[[ EPS is meant to help with Lua's floating point math in simple corner +cases like almostEquals(1.1-0.1, 1), which may not work as-is (e.g. on numbers +with rational binary representation) if the user doesn't provide some explicit +error margin. + +The default margin used by almostEquals() in such cases is EPS; and since +Lua may be compiled with different numeric precisions (single vs. double), we +try to select a useful default for it dynamically. Note: If the initial value +is not acceptable, it can be changed by the user to better suit specific needs. + +See also: https://en.wikipedia.org/wiki/Machine_epsilon +]] +M.EPS = 2^-52 -- = machine epsilon for "double", ~2.22E-16 +if math.abs(1.1 - 1 - 0.1) > M.EPS then + -- rounding error is above EPS, assume single precision + M.EPS = 2^-23 -- = machine epsilon for "float", ~1.19E-07 +end + +-- set this to false to debug luaunit +local STRIP_LUAUNIT_FROM_STACKTRACE = true + +M.VERBOSITY_DEFAULT = 10 +M.VERBOSITY_LOW = 1 +M.VERBOSITY_QUIET = 0 +M.VERBOSITY_VERBOSE = 20 +M.DEFAULT_DEEP_ANALYSIS = nil +M.FORCE_DEEP_ANALYSIS = true +M.DISABLE_DEEP_ANALYSIS = false + +-- set EXPORT_ASSERT_TO_GLOBALS to have all asserts visible as global values +-- EXPORT_ASSERT_TO_GLOBALS = true + +-- we need to keep a copy of the script args before it is overriden +local cmdline_argv = rawget(_G, "arg") + +M.FAILURE_PREFIX = 'LuaUnit test FAILURE: ' -- prefix string for failed tests +M.SUCCESS_PREFIX = 'LuaUnit test SUCCESS: ' -- prefix string for successful tests finished early +M.SKIP_PREFIX = 'LuaUnit test SKIP: ' -- prefix string for skipped tests + + + +M.USAGE=[[Usage: lua [options] [testname1 [testname2] ... ] +Options: + -h, --help: Print this help + --version: Print version information + -v, --verbose: Increase verbosity + -q, --quiet: Set verbosity to minimum + -e, --error: Stop on first error + -f, --failure: Stop on first failure or error + -s, --shuffle: Shuffle tests before running them + -o, --output OUTPUT: Set output type to OUTPUT + Possible values: text, tap, junit, nil + -n, --name NAME: For junit only, mandatory name of xml file + -r, --repeat NUM: Execute all tests NUM times, e.g. to trig the JIT + -p, --pattern PATTERN: Execute all test names matching the Lua PATTERN + May be repeated to include several patterns + Make sure you escape magic chars like +? with % + -x, --exclude PATTERN: Exclude all test names matching the Lua PATTERN + May be repeated to exclude several patterns + Make sure you escape magic chars like +? with % + testname1, testname2, ... : tests to run in the form of testFunction, + TestClass or TestClass.testMethod +]] + +local is_equal -- defined here to allow calling from mismatchFormattingPureList + +---------------------------------------------------------------- +-- +-- general utility functions +-- +---------------------------------------------------------------- + +local function pcall_or_abort(func, ...) + -- unpack is a global function for Lua 5.1, otherwise use table.unpack + local unpack = rawget(_G, "unpack") or table.unpack + local result = {pcall(func, ...)} + if not result[1] then + -- an error occurred + print(result[2]) -- error message + print() + print(M.USAGE) + os.exit(-1) + end + return unpack(result, 2) +end + +local crossTypeOrdering = { + number = 1, boolean = 2, string = 3, table = 4, other = 5 +} +local crossTypeComparison = { + number = function(a, b) return a < b end, + string = function(a, b) return a < b end, + other = function(a, b) return tostring(a) < tostring(b) end, +} + +local function crossTypeSort(a, b) + local type_a, type_b = type(a), type(b) + if type_a == type_b then + local func = crossTypeComparison[type_a] or crossTypeComparison.other + return func(a, b) + end + type_a = crossTypeOrdering[type_a] or crossTypeOrdering.other + type_b = crossTypeOrdering[type_b] or crossTypeOrdering.other + return type_a < type_b +end + +local function __genSortedIndex( t ) + -- Returns a sequence consisting of t's keys, sorted. + local sortedIndex = {} + + for key,_ in pairs(t) do + table.insert(sortedIndex, key) + end + + table.sort(sortedIndex, crossTypeSort) + return sortedIndex +end +M.private.__genSortedIndex = __genSortedIndex + +local function sortedNext(state, control) + -- Equivalent of the next() function of table iteration, but returns the + -- keys in sorted order (see __genSortedIndex and crossTypeSort). + -- The state is a temporary variable during iteration and contains the + -- sorted key table (state.sortedIdx). It also stores the last index (into + -- the keys) used by the iteration, to find the next one quickly. + local key + + --print("sortedNext: control = "..tostring(control) ) + if control == nil then + -- start of iteration + state.count = #state.sortedIdx + state.lastIdx = 1 + key = state.sortedIdx[1] + return key, state.t[key] + end + + -- normally, we expect the control variable to match the last key used + if control ~= state.sortedIdx[state.lastIdx] then + -- strange, we have to find the next value by ourselves + -- the key table is sorted in crossTypeSort() order! -> use bisection + local lower, upper = 1, state.count + repeat + state.lastIdx = math.modf((lower + upper) / 2) + key = state.sortedIdx[state.lastIdx] + if key == control then + break -- key found (and thus prev index) + end + if crossTypeSort(key, control) then + -- key < control, continue search "right" (towards upper bound) + lower = state.lastIdx + 1 + else + -- key > control, continue search "left" (towards lower bound) + upper = state.lastIdx - 1 + end + until lower > upper + if lower > upper then -- only true if the key wasn't found, ... + state.lastIdx = state.count -- ... so ensure no match in code below + end + end + + -- proceed by retrieving the next value (or nil) from the sorted keys + state.lastIdx = state.lastIdx + 1 + key = state.sortedIdx[state.lastIdx] + if key then + return key, state.t[key] + end + + -- getting here means returning `nil`, which will end the iteration +end + +local function sortedPairs(tbl) + -- Equivalent of the pairs() function on tables. Allows to iterate in + -- sorted order. As required by "generic for" loops, this will return the + -- iterator (function), an "invariant state", and the initial control value. + -- (see http://www.lua.org/pil/7.2.html) + return sortedNext, {t = tbl, sortedIdx = __genSortedIndex(tbl)}, nil +end +M.private.sortedPairs = sortedPairs + +-- seed the random with a strongly varying seed +math.randomseed(os.clock()*1E11) + +local function randomizeTable( t ) + -- randomize the item orders of the table t + for i = #t, 2, -1 do + local j = math.random(i) + if i ~= j then + t[i], t[j] = t[j], t[i] + end + end +end +M.private.randomizeTable = randomizeTable + +local function strsplit(delimiter, text) +-- Split text into a list consisting of the strings in text, separated +-- by strings matching delimiter (which may _NOT_ be a pattern). +-- Example: strsplit(", ", "Anna, Bob, Charlie, Dolores") + if delimiter == "" or delimiter == nil then -- this would result in endless loops + error("delimiter is nil or empty string!") + end + if text == nil then + return nil + end + + local list, pos, first, last = {}, 1 + while true do + first, last = text:find(delimiter, pos, true) + if first then -- found? + table.insert(list, text:sub(pos, first - 1)) + pos = last + 1 + else + table.insert(list, text:sub(pos)) + break + end + end + return list +end +M.private.strsplit = strsplit + +local function hasNewLine( s ) + -- return true if s has a newline + return (string.find(s, '\n', 1, true) ~= nil) +end +M.private.hasNewLine = hasNewLine + +local function prefixString( prefix, s ) + -- Prefix all the lines of s with prefix + return prefix .. string.gsub(s, '\n', '\n' .. prefix) +end +M.private.prefixString = prefixString + +local function strMatch(s, pattern, start, final ) + -- return true if s matches completely the pattern from index start to index end + -- return false in every other cases + -- if start is nil, matches from the beginning of the string + -- if final is nil, matches to the end of the string + start = start or 1 + final = final or string.len(s) + + local foundStart, foundEnd = string.find(s, pattern, start, false) + return foundStart == start and foundEnd == final +end +M.private.strMatch = strMatch + +local function patternFilter(patterns, expr) + -- Run `expr` through the inclusion and exclusion rules defined in patterns + -- and return true if expr shall be included, false for excluded. + -- Inclusion pattern are defined as normal patterns, exclusions + -- patterns start with `!` and are followed by a normal pattern + + -- result: nil = UNKNOWN (not matched yet), true = ACCEPT, false = REJECT + -- default: true if no explicit "include" is found, set to false otherwise + local default, result = true, nil + + if patterns ~= nil then + for _, pattern in ipairs(patterns) do + local exclude = pattern:sub(1,1) == '!' + if exclude then + pattern = pattern:sub(2) + else + -- at least one include pattern specified, a match is required + default = false + end + -- print('pattern: ',pattern) + -- print('exclude: ',exclude) + -- print('default: ',default) + + if string.find(expr, pattern) then + -- set result to false when excluding, true otherwise + result = not exclude + end + end + end + + if result ~= nil then + return result + end + return default +end +M.private.patternFilter = patternFilter + +local function xmlEscape( s ) + -- Return s escaped for XML attributes + -- escapes table: + -- " " + -- ' ' + -- < < + -- > > + -- & & + + return string.gsub( s, '.', { + ['&'] = "&", + ['"'] = """, + ["'"] = "'", + ['<'] = "<", + ['>'] = ">", + } ) +end +M.private.xmlEscape = xmlEscape + +local function xmlCDataEscape( s ) + -- Return s escaped for CData section, escapes: "]]>" + return string.gsub( s, ']]>', ']]>' ) +end +M.private.xmlCDataEscape = xmlCDataEscape + + +local function lstrip( s ) + --[[Return s with all leading white spaces and tabs removed]] + local idx = 0 + while idx < s:len() do + idx = idx + 1 + local c = s:sub(idx,idx) + if c ~= ' ' and c ~= '\t' then + break + end + end + return s:sub(idx) +end +M.private.lstrip = lstrip + +local function extractFileLineInfo( s ) + --[[ From a string in the form "(leading spaces) dir1/dir2\dir3\file.lua:linenb: msg" + + Return the "file.lua:linenb" information + ]] + local s2 = lstrip(s) + local firstColon = s2:find(':', 1, true) + if firstColon == nil then + -- string is not in the format file:line: + return s + end + local secondColon = s2:find(':', firstColon+1, true) + if secondColon == nil then + -- string is not in the format file:line: + return s + end + + return s2:sub(1, secondColon-1) +end +M.private.extractFileLineInfo = extractFileLineInfo + + +local function stripLuaunitTrace2( stackTrace, errMsg ) + --[[ + -- Example of a traceback: + < + [C]: in function 'xpcall' + ./luaunit.lua:1449: in function 'protectedCall' + ./luaunit.lua:1508: in function 'execOneFunction' + ./luaunit.lua:1596: in function 'runSuiteByInstances' + ./luaunit.lua:1660: in function 'runSuiteByNames' + ./luaunit.lua:1736: in function 'runSuite' + example_with_luaunit.lua:140: in main chunk + [C]: in ?>> + error message: <> + + Other example: + < + [C]: in function 'xpcall' + ./luaunit.lua:1517: in function 'protectedCall' + ./luaunit.lua:1578: in function 'execOneFunction' + ./luaunit.lua:1677: in function 'runSuiteByInstances' + ./luaunit.lua:1730: in function 'runSuiteByNames' + ./luaunit.lua:1806: in function 'runSuite' + example_with_luaunit.lua:140: in main chunk + [C]: in ?>> + error message: <> + + < + [C]: in function 'xpcall' + luaunit2/luaunit.lua:1532: in function 'protectedCall' + luaunit2/luaunit.lua:1591: in function 'execOneFunction' + luaunit2/luaunit.lua:1679: in function 'runSuiteByInstances' + luaunit2/luaunit.lua:1743: in function 'runSuiteByNames' + luaunit2/luaunit.lua:1819: in function 'runSuite' + luaunit2/example_with_luaunit.lua:140: in main chunk + [C]: in ?>> + error message: <> + + + -- first line is "stack traceback": KEEP + -- next line may be luaunit line: REMOVE + -- next lines are call in the program under testOk: REMOVE + -- next lines are calls from luaunit to call the program under test: KEEP + + -- Strategy: + -- keep first line + -- remove lines that are part of luaunit + -- kepp lines until we hit a luaunit line + + The strategy for stripping is: + * keep first line "stack traceback:" + * part1: + * analyse all lines of the stack from bottom to top of the stack (first line to last line) + * extract the "file:line:" part of the line + * compare it with the "file:line" part of the error message + * if it does not match strip the line + * if it matches, keep the line and move to part 2 + * part2: + * anything NOT starting with luaunit.lua is the interesting part of the stack trace + * anything starting again with luaunit.lua is part of the test launcher and should be stripped out + ]] + + local function isLuaunitInternalLine( s ) + -- return true if line of stack trace comes from inside luaunit + return s:find('[/\\]luaunit%.lua:%d+: ') ~= nil + end + + -- print( '<<'..stackTrace..'>>' ) + + local t = strsplit( '\n', stackTrace ) + -- print( prettystr(t) ) + + local idx = 2 + + local errMsgFileLine = extractFileLineInfo(errMsg) + -- print('emfi="'..errMsgFileLine..'"') + + -- remove lines that are still part of luaunit + while t[idx] and extractFileLineInfo(t[idx]) ~= errMsgFileLine do + -- print('Removing : '..t[idx] ) + table.remove(t, idx) + end + + -- keep lines until we hit luaunit again + while t[idx] and (not isLuaunitInternalLine(t[idx])) do + -- print('Keeping : '..t[idx] ) + idx = idx + 1 + end + + -- remove remaining luaunit lines + while t[idx] do + -- print('Removing2 : '..t[idx] ) + table.remove(t, idx) + end + + -- print( prettystr(t) ) + return table.concat( t, '\n') + +end +M.private.stripLuaunitTrace2 = stripLuaunitTrace2 + + +local function prettystr_sub(v, indentLevel, printTableRefs, cycleDetectTable ) + local type_v = type(v) + if "string" == type_v then + -- use clever delimiters according to content: + -- enclose with single quotes if string contains ", but no ' + if v:find('"', 1, true) and not v:find("'", 1, true) then + return "'" .. v .. "'" + end + -- use double quotes otherwise, escape embedded " + return '"' .. v:gsub('"', '\\"') .. '"' + + elseif "table" == type_v then + --if v.__class__ then + -- return string.gsub( tostring(v), 'table', v.__class__ ) + --end + return M.private._table_tostring(v, indentLevel, printTableRefs, cycleDetectTable) + + elseif "number" == type_v then + -- eliminate differences in formatting between various Lua versions + if v ~= v then + return "#NaN" -- "not a number" + end + if v == math.huge then + return "#Inf" -- "infinite" + end + if v == -math.huge then + return "-#Inf" + end + if _VERSION == "Lua 5.3" then + local i = math.tointeger(v) + if i then + return tostring(i) + end + end + end + + return tostring(v) +end + +local function prettystr( v ) + --[[ Pretty string conversion, to display the full content of a variable of any type. + + * string are enclosed with " by default, or with ' if string contains a " + * tables are expanded to show their full content, with indentation in case of nested tables + ]]-- + local cycleDetectTable = {} + local s = prettystr_sub(v, 1, M.PRINT_TABLE_REF_IN_ERROR_MSG, cycleDetectTable) + if cycleDetectTable.detected and not M.PRINT_TABLE_REF_IN_ERROR_MSG then + -- some table contain recursive references, + -- so we must recompute the value by including all table references + -- else the result looks like crap + cycleDetectTable = {} + s = prettystr_sub(v, 1, true, cycleDetectTable) + end + return s +end +M.prettystr = prettystr + +function M.adjust_err_msg_with_iter( err_msg, iter_msg ) + --[[ Adjust the error message err_msg: trim the FAILURE_PREFIX or SUCCESS_PREFIX information if needed, + add the iteration message if any and return the result. + + err_msg: string, error message captured with pcall + iter_msg: a string describing the current iteration ("iteration N") or nil + if there is no iteration in this test. + + Returns: (new_err_msg, test_status) + new_err_msg: string, adjusted error message, or nil in case of success + test_status: M.NodeStatus.FAIL, SUCCESS or ERROR according to the information + contained in the error message. + ]] + if iter_msg then + iter_msg = iter_msg..', ' + else + iter_msg = '' + end + + local RE_FILE_LINE = '.*:%d+: ' + + -- error message is not necessarily a string, + -- so convert the value to string with prettystr() + if type( err_msg ) ~= 'string' then + err_msg = prettystr( err_msg ) + end + + if (err_msg:find( M.SUCCESS_PREFIX ) == 1) or err_msg:match( '('..RE_FILE_LINE..')' .. M.SUCCESS_PREFIX .. ".*" ) then + -- test finished early with success() + return nil, M.NodeStatus.SUCCESS + end + + if (err_msg:find( M.SKIP_PREFIX ) == 1) or (err_msg:match( '('..RE_FILE_LINE..')' .. M.SKIP_PREFIX .. ".*" ) ~= nil) then + -- substitute prefix by iteration message + err_msg = err_msg:gsub('.*'..M.SKIP_PREFIX, iter_msg, 1) + -- print("failure detected") + return err_msg, M.NodeStatus.SKIP + end + + if (err_msg:find( M.FAILURE_PREFIX ) == 1) or (err_msg:match( '('..RE_FILE_LINE..')' .. M.FAILURE_PREFIX .. ".*" ) ~= nil) then + -- substitute prefix by iteration message + err_msg = err_msg:gsub(M.FAILURE_PREFIX, iter_msg, 1) + -- print("failure detected") + return err_msg, M.NodeStatus.FAIL + end + + + + -- print("error detected") + -- regular error, not a failure + if iter_msg then + local match + -- "./test\\test_luaunit.lua:2241: some error msg + match = err_msg:match( '(.*:%d+: ).*' ) + if match then + err_msg = err_msg:gsub( match, match .. iter_msg ) + else + -- no file:line: infromation, just add the iteration info at the beginning of the line + err_msg = iter_msg .. err_msg + end + end + return err_msg, M.NodeStatus.ERROR +end + +local function tryMismatchFormatting( table_a, table_b, doDeepAnalysis ) + --[[ + Prepares a nice error message when comparing tables, performing a deeper + analysis. + + Arguments: + * table_a, table_b: tables to be compared + * doDeepAnalysis: + M.DEFAULT_DEEP_ANALYSIS: (the default if not specified) perform deep analysis only for big lists and big dictionnaries + M.FORCE_DEEP_ANALYSIS : always perform deep analysis + M.DISABLE_DEEP_ANALYSIS: never perform deep analysis + + Returns: {success, result} + * success: false if deep analysis could not be performed + in this case, just use standard assertion message + * result: if success is true, a multi-line string with deep analysis of the two lists + ]] + + -- check if table_a & table_b are suitable for deep analysis + if type(table_a) ~= 'table' or type(table_b) ~= 'table' then + return false + end + + if doDeepAnalysis == M.DISABLE_DEEP_ANALYSIS then + return false + end + + local len_a, len_b, isPureList = #table_a, #table_b, true + + for k1, v1 in pairs(table_a) do + if type(k1) ~= 'number' or k1 > len_a then + -- this table a mapping + isPureList = false + break + end + end + + if isPureList then + for k2, v2 in pairs(table_b) do + if type(k2) ~= 'number' or k2 > len_b then + -- this table a mapping + isPureList = false + break + end + end + end + + if isPureList and math.min(len_a, len_b) < M.LIST_DIFF_ANALYSIS_THRESHOLD then + if not (doDeepAnalysis == M.FORCE_DEEP_ANALYSIS) then + return false + end + end + + if isPureList then + return M.private.mismatchFormattingPureList( table_a, table_b ) + else + -- only work on mapping for the moment + -- return M.private.mismatchFormattingMapping( table_a, table_b, doDeepAnalysis ) + return false + end +end +M.private.tryMismatchFormatting = tryMismatchFormatting + +local function getTaTbDescr() + if not M.ORDER_ACTUAL_EXPECTED then + return 'expected', 'actual' + end + return 'actual', 'expected' +end + +local function extendWithStrFmt( res, ... ) + table.insert( res, string.format( ... ) ) +end + +local function mismatchFormattingMapping( table_a, table_b, doDeepAnalysis ) + --[[ + Prepares a nice error message when comparing tables which are not pure lists, performing a deeper + analysis. + + Returns: {success, result} + * success: false if deep analysis could not be performed + in this case, just use standard assertion message + * result: if success is true, a multi-line string with deep analysis of the two lists + ]] + + -- disable for the moment + --[[ + local result = {} + local descrTa, descrTb = getTaTbDescr() + + local keysCommon = {} + local keysOnlyTa = {} + local keysOnlyTb = {} + local keysDiffTaTb = {} + + local k, v + + for k,v in pairs( table_a ) do + if is_equal( v, table_b[k] ) then + table.insert( keysCommon, k ) + else + if table_b[k] == nil then + table.insert( keysOnlyTa, k ) + else + table.insert( keysDiffTaTb, k ) + end + end + end + + for k,v in pairs( table_b ) do + if not is_equal( v, table_a[k] ) and table_a[k] == nil then + table.insert( keysOnlyTb, k ) + end + end + + local len_a = #keysCommon + #keysDiffTaTb + #keysOnlyTa + local len_b = #keysCommon + #keysDiffTaTb + #keysOnlyTb + local limited_display = (len_a < 5 or len_b < 5) + + if math.min(len_a, len_b) < M.TABLE_DIFF_ANALYSIS_THRESHOLD then + return false + end + + if not limited_display then + if len_a == len_b then + extendWithStrFmt( result, 'Table A (%s) and B (%s) both have %d items', descrTa, descrTb, len_a ) + else + extendWithStrFmt( result, 'Table A (%s) has %d items and table B (%s) has %d items', descrTa, len_a, descrTb, len_b ) + end + + if #keysCommon == 0 and #keysDiffTaTb == 0 then + table.insert( result, 'Table A and B have no keys in common, they are totally different') + else + local s_other = 'other ' + if #keysCommon then + extendWithStrFmt( result, 'Table A and B have %d identical items', #keysCommon ) + else + table.insert( result, 'Table A and B have no identical items' ) + s_other = '' + end + + if #keysDiffTaTb ~= 0 then + result[#result] = string.format( '%s and %d items differing present in both tables', result[#result], #keysDiffTaTb) + else + result[#result] = string.format( '%s and no %sitems differing present in both tables', result[#result], s_other, #keysDiffTaTb) + end + end + + extendWithStrFmt( result, 'Table A has %d keys not present in table B and table B has %d keys not present in table A', #keysOnlyTa, #keysOnlyTb ) + end + + local function keytostring(k) + if "string" == type(k) and k:match("^[_%a][_%w]*$") then + return k + end + return prettystr(k) + end + + if #keysDiffTaTb ~= 0 then + table.insert( result, 'Items differing in A and B:') + for k,v in sortedPairs( keysDiffTaTb ) do + extendWithStrFmt( result, ' - A[%s]: %s', keytostring(v), prettystr(table_a[v]) ) + extendWithStrFmt( result, ' + B[%s]: %s', keytostring(v), prettystr(table_b[v]) ) + end + end + + if #keysOnlyTa ~= 0 then + table.insert( result, 'Items only in table A:' ) + for k,v in sortedPairs( keysOnlyTa ) do + extendWithStrFmt( result, ' - A[%s]: %s', keytostring(v), prettystr(table_a[v]) ) + end + end + + if #keysOnlyTb ~= 0 then + table.insert( result, 'Items only in table B:' ) + for k,v in sortedPairs( keysOnlyTb ) do + extendWithStrFmt( result, ' + B[%s]: %s', keytostring(v), prettystr(table_b[v]) ) + end + end + + if #keysCommon ~= 0 then + table.insert( result, 'Items common to A and B:') + for k,v in sortedPairs( keysCommon ) do + extendWithStrFmt( result, ' = A and B [%s]: %s', keytostring(v), prettystr(table_a[v]) ) + end + end + + return true, table.concat( result, '\n') + ]] +end +M.private.mismatchFormattingMapping = mismatchFormattingMapping + +local function mismatchFormattingPureList( table_a, table_b ) + --[[ + Prepares a nice error message when comparing tables which are lists, performing a deeper + analysis. + + Returns: {success, result} + * success: false if deep analysis could not be performed + in this case, just use standard assertion message + * result: if success is true, a multi-line string with deep analysis of the two lists + ]] + local result, descrTa, descrTb = {}, getTaTbDescr() + + local len_a, len_b, refa, refb = #table_a, #table_b, '', '' + if M.PRINT_TABLE_REF_IN_ERROR_MSG then + refa, refb = string.format( '<%s> ', M.private.table_ref(table_a)), string.format('<%s> ', M.private.table_ref(table_b) ) + end + local longest, shortest = math.max(len_a, len_b), math.min(len_a, len_b) + local deltalv = longest - shortest + + local commonUntil = shortest + for i = 1, shortest do + if not is_equal(table_a[i], table_b[i]) then + commonUntil = i - 1 + break + end + end + + local commonBackTo = shortest - 1 + for i = 0, shortest - 1 do + if not is_equal(table_a[len_a-i], table_b[len_b-i]) then + commonBackTo = i - 1 + break + end + end + + + table.insert( result, 'List difference analysis:' ) + if len_a == len_b then + -- TODO: handle expected/actual naming + extendWithStrFmt( result, '* lists %sA (%s) and %sB (%s) have the same size', refa, descrTa, refb, descrTb ) + else + extendWithStrFmt( result, '* list sizes differ: list %sA (%s) has %d items, list %sB (%s) has %d items', refa, descrTa, len_a, refb, descrTb, len_b ) + end + + extendWithStrFmt( result, '* lists A and B start differing at index %d', commonUntil+1 ) + if commonBackTo >= 0 then + if deltalv > 0 then + extendWithStrFmt( result, '* lists A and B are equal again from index %d for A, %d for B', len_a-commonBackTo, len_b-commonBackTo ) + else + extendWithStrFmt( result, '* lists A and B are equal again from index %d', len_a-commonBackTo ) + end + end + + local function insertABValue(ai, bi) + bi = bi or ai + if is_equal( table_a[ai], table_b[bi]) then + return extendWithStrFmt( result, ' = A[%d], B[%d]: %s', ai, bi, prettystr(table_a[ai]) ) + else + extendWithStrFmt( result, ' - A[%d]: %s', ai, prettystr(table_a[ai])) + extendWithStrFmt( result, ' + B[%d]: %s', bi, prettystr(table_b[bi])) + end + end + + -- common parts to list A & B, at the beginning + if commonUntil > 0 then + table.insert( result, '* Common parts:' ) + for i = 1, commonUntil do + insertABValue( i ) + end + end + + -- diffing parts to list A & B + if commonUntil < shortest - commonBackTo - 1 then + table.insert( result, '* Differing parts:' ) + for i = commonUntil + 1, shortest - commonBackTo - 1 do + insertABValue( i ) + end + end + + -- display indexes of one list, with no match on other list + if shortest - commonBackTo <= longest - commonBackTo - 1 then + table.insert( result, '* Present only in one list:' ) + for i = shortest - commonBackTo, longest - commonBackTo - 1 do + if len_a > len_b then + extendWithStrFmt( result, ' - A[%d]: %s', i, prettystr(table_a[i]) ) + -- table.insert( result, '+ (no matching B index)') + else + -- table.insert( result, '- no matching A index') + extendWithStrFmt( result, ' + B[%d]: %s', i, prettystr(table_b[i]) ) + end + end + end + + -- common parts to list A & B, at the end + if commonBackTo >= 0 then + table.insert( result, '* Common parts at the end of the lists' ) + for i = longest - commonBackTo, longest do + if len_a > len_b then + insertABValue( i, i-deltalv ) + else + insertABValue( i-deltalv, i ) + end + end + end + + return true, table.concat( result, '\n') +end +M.private.mismatchFormattingPureList = mismatchFormattingPureList + +local function prettystrPairs(value1, value2, suffix_a, suffix_b) + --[[ + This function helps with the recurring task of constructing the "expected + vs. actual" error messages. It takes two arbitrary values and formats + corresponding strings with prettystr(). + + To keep the (possibly complex) output more readable in case the resulting + strings contain line breaks, they get automatically prefixed with additional + newlines. Both suffixes are optional (default to empty strings), and get + appended to the "value1" string. "suffix_a" is used if line breaks were + encountered, "suffix_b" otherwise. + + Returns the two formatted strings (including padding/newlines). + ]] + local str1, str2 = prettystr(value1), prettystr(value2) + if hasNewLine(str1) or hasNewLine(str2) then + -- line break(s) detected, add padding + return "\n" .. str1 .. (suffix_a or ""), "\n" .. str2 + end + return str1 .. (suffix_b or ""), str2 +end +M.private.prettystrPairs = prettystrPairs + +local UNKNOWN_REF = 'table 00-unknown ref' +local ref_generator = { value=1, [UNKNOWN_REF]=0 } + +local function table_ref( t ) + -- return the default tostring() for tables, with the table ID, even if the table has a metatable + -- with the __tostring converter + local ref = '' + local mt = getmetatable( t ) + if mt == nil then + ref = tostring(t) + else + local success, result + success, result = pcall(setmetatable, t, nil) + if not success then + -- protected table, if __tostring is defined, we can + -- not get the reference. And we can not know in advance. + ref = tostring(t) + if not ref:match( 'table: 0?x?[%x]+' ) then + return UNKNOWN_REF + end + else + ref = tostring(t) + setmetatable( t, mt ) + end + end + -- strip the "table: " part + ref = ref:sub(8) + if ref ~= UNKNOWN_REF and ref_generator[ref] == nil then + -- Create a new reference number + ref_generator[ref] = ref_generator.value + ref_generator.value = ref_generator.value+1 + end + if M.PRINT_TABLE_REF_IN_ERROR_MSG then + return string.format('table %02d-%s', ref_generator[ref], ref) + else + return string.format('table %02d', ref_generator[ref]) + end +end +M.private.table_ref = table_ref + +local TABLE_TOSTRING_SEP = ", " +local TABLE_TOSTRING_SEP_LEN = string.len(TABLE_TOSTRING_SEP) + +local function _table_tostring( tbl, indentLevel, printTableRefs, cycleDetectTable ) + printTableRefs = printTableRefs or M.PRINT_TABLE_REF_IN_ERROR_MSG + cycleDetectTable = cycleDetectTable or {} + cycleDetectTable[tbl] = true + + local result, dispOnMultLines = {}, false + + -- like prettystr but do not enclose with "" if the string is just alphanumerical + -- this is better for displaying table keys who are often simple strings + local function keytostring(k) + if "string" == type(k) and k:match("^[_%a][_%w]*$") then + return k + end + return prettystr_sub(k, indentLevel+1, printTableRefs, cycleDetectTable) + end + + local mt = getmetatable( tbl ) + + if mt and mt.__tostring then + -- if table has a __tostring() function in its metatable, use it to display the table + -- else, compute a regular table + result = tostring(tbl) + if type(result) ~= 'string' then + return string.format( '', prettystr(result) ) + end + result = strsplit( '\n', result ) + return M.private._table_tostring_format_multiline_string( result, indentLevel ) + + else + -- no metatable, compute the table representation + + local entry, count, seq_index = nil, 0, 1 + for k, v in sortedPairs( tbl ) do + + -- key part + if k == seq_index then + -- for the sequential part of tables, we'll skip the "=" output + entry = '' + seq_index = seq_index + 1 + elseif cycleDetectTable[k] then + -- recursion in the key detected + cycleDetectTable.detected = true + entry = "<"..table_ref(k)..">=" + else + entry = keytostring(k) .. "=" + end + + -- value part + if cycleDetectTable[v] then + -- recursion in the value detected! + cycleDetectTable.detected = true + entry = entry .. "<"..table_ref(v)..">" + else + entry = entry .. + prettystr_sub( v, indentLevel+1, printTableRefs, cycleDetectTable ) + end + count = count + 1 + result[count] = entry + end + return M.private._table_tostring_format_result( tbl, result, indentLevel, printTableRefs ) + end + +end +M.private._table_tostring = _table_tostring -- prettystr_sub() needs it + +local function _table_tostring_format_multiline_string( tbl_str, indentLevel ) + local indentString = '\n'..string.rep(" ", indentLevel - 1) + return table.concat( tbl_str, indentString ) + +end +M.private._table_tostring_format_multiline_string = _table_tostring_format_multiline_string + + +local function _table_tostring_format_result( tbl, result, indentLevel, printTableRefs ) + -- final function called in _table_to_string() to format the resulting list of + -- string describing the table. + + local dispOnMultLines = false + + -- set dispOnMultLines to true if the maximum LINE_LENGTH would be exceeded with the values + local totalLength = 0 + for k, v in ipairs( result ) do + totalLength = totalLength + string.len( v ) + if totalLength >= M.LINE_LENGTH then + dispOnMultLines = true + break + end + end + + -- set dispOnMultLines to true if the max LINE_LENGTH would be exceeded + -- with the values and the separators. + if not dispOnMultLines then + -- adjust with length of separator(s): + -- two items need 1 sep, three items two seps, ... plus len of '{}' + if #result > 0 then + totalLength = totalLength + TABLE_TOSTRING_SEP_LEN * (#result - 1) + end + dispOnMultLines = (totalLength + 2 >= M.LINE_LENGTH) + end + + -- now reformat the result table (currently holding element strings) + if dispOnMultLines then + local indentString = string.rep(" ", indentLevel - 1) + result = { + "{\n ", + indentString, + table.concat(result, ",\n " .. indentString), + "\n", + indentString, + "}" + } + else + result = {"{", table.concat(result, TABLE_TOSTRING_SEP), "}"} + end + if printTableRefs then + table.insert(result, 1, "<"..table_ref(tbl).."> ") -- prepend table ref + end + return table.concat(result) +end +M.private._table_tostring_format_result = _table_tostring_format_result -- prettystr_sub() needs it + +local function table_findkeyof(t, element) + -- Return the key k of the given element in table t, so that t[k] == element + -- (or `nil` if element is not present within t). Note that we use our + -- 'general' is_equal comparison for matching, so this function should + -- handle table-type elements gracefully and consistently. + if type(t) == "table" then + for k, v in pairs(t) do + if is_equal(v, element) then + return k + end + end + end + return nil +end + +local function _is_table_items_equals(actual, expected ) + local type_a, type_e = type(actual), type(expected) + + if type_a ~= type_e then + return false + + elseif (type_a == 'table') --[[and (type_e == 'table')]] then + for k, v in pairs(actual) do + if table_findkeyof(expected, v) == nil then + return false -- v not contained in expected + end + end + for k, v in pairs(expected) do + if table_findkeyof(actual, v) == nil then + return false -- v not contained in actual + end + end + return true + + elseif actual ~= expected then + return false + end + + return true +end + +--[[ +This is a specialized metatable to help with the bookkeeping of recursions +in _is_table_equals(). It provides an __index table that implements utility +functions for easier management of the table. The "cached" method queries +the state of a specific (actual,expected) pair; and the "store" method sets +this state to the given value. The state of pairs not "seen" / visited is +assumed to be `nil`. +]] +local _recursion_cache_MT = { + __index = { + -- Return the cached value for an (actual,expected) pair (or `nil`) + cached = function(t, actual, expected) + local subtable = t[actual] or {} + return subtable[expected] + end, + + -- Store cached value for a specific (actual,expected) pair. + -- Returns the value, so it's easy to use for a "tailcall" (return ...). + store = function(t, actual, expected, value, asymmetric) + local subtable = t[actual] + if not subtable then + subtable = {} + t[actual] = subtable + end + subtable[expected] = value + + -- Unless explicitly marked "asymmetric": Consider the recursion + -- on (expected,actual) to be equivalent to (actual,expected) by + -- default, and thus cache the value for both. + if not asymmetric then + t:store(expected, actual, value, true) + end + + return value + end + } +} + +local function _is_table_equals(actual, expected, cycleDetectTable) + local type_a, type_e = type(actual), type(expected) + + if type_a ~= type_e then + return false -- different types won't match + end + + if type_a ~= 'table' then + -- other typtes compare directly + return actual == expected + end + + -- print('_is_table_equals( \n '..prettystr(actual)..'\n , '..prettystr(expected)..'\n , '..prettystr(recursions)..' \n )') + + cycleDetectTable = cycleDetectTable or { actual={}, expected={} } + if cycleDetectTable.actual[ actual ] then + -- oh, we hit a cycle in actual + if cycleDetectTable.expected[ expected ] then + -- uh, we hit a cycle at the same time in expected + -- so the two tables have similar structure + return true + end + + -- cycle was hit only in actual, the structure differs from expected + return false + end + + if cycleDetectTable.expected[ expected ] then + -- no cycle in actual, but cycle in expected + -- the structure differ + return false + end + + -- at this point, no table cycle detected, we are + -- seeing this table for the first time + + -- mark the cycle detection + cycleDetectTable.actual[ actual ] = true + cycleDetectTable.expected[ expected ] = true + + + local actualKeysMatched = {} + for k, v in pairs(actual) do + actualKeysMatched[k] = true -- Keep track of matched keys + if not _is_table_equals(v, expected[k], cycleDetectTable) then + -- table differs on this key + -- clear the cycle detection before returning + cycleDetectTable.actual[ actual ] = nil + cycleDetectTable.expected[ expected ] = nil + return false + end + end + + for k, v in pairs(expected) do + if not actualKeysMatched[k] then + -- Found a key that we did not see in "actual" -> mismatch + -- clear the cycle detection before returning + cycleDetectTable.actual[ actual ] = nil + cycleDetectTable.expected[ expected ] = nil + return false + end + -- Otherwise actual[k] was already matched against v = expected[k]. + end + + -- all key match, we have a match ! + cycleDetectTable.actual[ actual ] = nil + cycleDetectTable.expected[ expected ] = nil + return true +end +M.private._is_table_equals = _is_table_equals +is_equal = _is_table_equals + +local function failure(main_msg, extra_msg_or_nil, level) + -- raise an error indicating a test failure + -- for error() compatibility we adjust "level" here (by +1), to report the + -- calling context + local msg + if type(extra_msg_or_nil) == 'string' and extra_msg_or_nil:len() > 0 then + msg = extra_msg_or_nil .. '\n' .. main_msg + else + msg = main_msg + end + error(M.FAILURE_PREFIX .. msg, (level or 1) + 1) +end + +local function fail_fmt(level, extra_msg_or_nil, ...) + -- failure with printf-style formatted message and given error level + failure(string.format(...), extra_msg_or_nil, (level or 1) + 1) +end +M.private.fail_fmt = fail_fmt + +local function error_fmt(level, ...) + -- printf-style error() + error(string.format(...), (level or 1) + 1) +end + +---------------------------------------------------------------- +-- +-- assertions +-- +---------------------------------------------------------------- + +local function errorMsgEquality(actual, expected, doDeepAnalysis) + + if not M.ORDER_ACTUAL_EXPECTED then + expected, actual = actual, expected + end + if type(expected) == 'string' or type(expected) == 'table' then + local strExpected, strActual = prettystrPairs(expected, actual) + local result = string.format("expected: %s\nactual: %s", strExpected, strActual) + + -- extend with mismatch analysis if possible: + local success, mismatchResult + success, mismatchResult = tryMismatchFormatting( actual, expected, doDeepAnalysis ) + if success then + result = table.concat( { result, mismatchResult }, '\n' ) + end + return result + end + return string.format("expected: %s, actual: %s", + prettystr(expected), prettystr(actual)) +end + +function M.assertError(f, ...) + -- assert that calling f with the arguments will raise an error + -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error + if pcall( f, ... ) then + failure( "Expected an error when calling function but no error generated", nil, 2 ) + end +end + +function M.fail( msg ) + -- stops a test due to a failure + failure( msg, nil, 2 ) +end + +function M.failIf( cond, msg ) + -- Fails a test with "msg" if condition is true + if cond then + failure( msg, nil, 2 ) + end +end + +function M.skip(msg) + -- skip a running test + error_fmt(2, M.SKIP_PREFIX .. msg) +end + +function M.skipIf( cond, msg ) + -- skip a running test if condition is met + if cond then + error_fmt(2, M.SKIP_PREFIX .. msg) + end +end + +function M.runOnlyIf( cond, msg ) + -- continue a running test if condition is met, else skip it + if not cond then + error_fmt(2, M.SKIP_PREFIX .. prettystr(msg)) + end +end + +function M.success() + -- stops a test with a success + error_fmt(2, M.SUCCESS_PREFIX) +end + +function M.successIf( cond ) + -- stops a test with a success if condition is met + if cond then + error_fmt(2, M.SUCCESS_PREFIX) + end +end + + +------------------------------------------------------------------ +-- Equality assertions +------------------------------------------------------------------ + +function M.assertEquals(actual, expected, extra_msg_or_nil, doDeepAnalysis) + if type(actual) == 'table' and type(expected) == 'table' then + if not _is_table_equals(actual, expected) then + failure( errorMsgEquality(actual, expected, doDeepAnalysis), extra_msg_or_nil, 2 ) + end + elseif type(actual) ~= type(expected) then + failure( errorMsgEquality(actual, expected), extra_msg_or_nil, 2 ) + elseif actual ~= expected then + failure( errorMsgEquality(actual, expected), extra_msg_or_nil, 2 ) + end +end + +function M.almostEquals( actual, expected, margin ) + if type(actual) ~= 'number' or type(expected) ~= 'number' or type(margin) ~= 'number' then + error_fmt(3, 'almostEquals: must supply only number arguments.\nArguments supplied: %s, %s, %s', + prettystr(actual), prettystr(expected), prettystr(margin)) + end + if margin < 0 then + error_fmt(3, 'almostEquals: margin must not be negative, current value is ' .. margin) + end + return math.abs(expected - actual) <= margin +end + +function M.assertAlmostEquals( actual, expected, margin, extra_msg_or_nil ) + -- check that two floats are close by margin + margin = margin or M.EPS + if not M.almostEquals(actual, expected, margin) then + if not M.ORDER_ACTUAL_EXPECTED then + expected, actual = actual, expected + end + local delta = math.abs(actual - expected) + fail_fmt(2, extra_msg_or_nil, 'Values are not almost equal\n' .. + 'Actual: %s, expected: %s, delta %s above margin of %s', + actual, expected, delta, margin) + end +end + +function M.assertNotEquals(actual, expected, extra_msg_or_nil) + if type(actual) ~= type(expected) then + return + end + + if type(actual) == 'table' and type(expected) == 'table' then + if not _is_table_equals(actual, expected) then + return + end + elseif actual ~= expected then + return + end + fail_fmt(2, extra_msg_or_nil, 'Received the not expected value: %s', prettystr(actual)) +end + +function M.assertNotAlmostEquals( actual, expected, margin, extra_msg_or_nil ) + -- check that two floats are not close by margin + margin = margin or M.EPS + if M.almostEquals(actual, expected, margin) then + if not M.ORDER_ACTUAL_EXPECTED then + expected, actual = actual, expected + end + local delta = math.abs(actual - expected) + fail_fmt(2, extra_msg_or_nil, 'Values are almost equal\nActual: %s, expected: %s' .. + ', delta %s below margin of %s', + actual, expected, delta, margin) + end +end + +function M.assertItemsEquals(actual, expected, extra_msg_or_nil) + -- checks that the items of table expected + -- are contained in table actual. Warning, this function + -- is at least O(n^2) + if not _is_table_items_equals(actual, expected ) then + expected, actual = prettystrPairs(expected, actual) + fail_fmt(2, extra_msg_or_nil, 'Content of the tables are not identical:\nExpected: %s\nActual: %s', + expected, actual) + end +end + +------------------------------------------------------------------ +-- String assertion +------------------------------------------------------------------ + +function M.assertStrContains( str, sub, isPattern, extra_msg_or_nil ) + -- this relies on lua string.find function + -- a string always contains the empty string + -- assert( type(str) == 'string', 'Argument 1 of assertStrContains() should be a string.' ) ) + -- assert( type(sub) == 'string', 'Argument 2 of assertStrContains() should be a string.' ) ) + if not string.find(str, sub, 1, not isPattern) then + sub, str = prettystrPairs(sub, str, '\n') + fail_fmt(2, extra_msg_or_nil, 'Could not find %s %s in string %s', + isPattern and 'pattern' or 'substring', sub, str) + end +end + +function M.assertStrIContains( str, sub, extra_msg_or_nil ) + -- this relies on lua string.find function + -- a string always contains the empty string + if not string.find(str:lower(), sub:lower(), 1, true) then + sub, str = prettystrPairs(sub, str, '\n') + fail_fmt(2, extra_msg_or_nil, 'Could not find (case insensitively) substring %s in string %s', + sub, str) + end +end + +function M.assertNotStrContains( str, sub, isPattern, extra_msg_or_nil ) + -- this relies on lua string.find function + -- a string always contains the empty string + if string.find(str, sub, 1, not isPattern) then + sub, str = prettystrPairs(sub, str, '\n') + fail_fmt(2, extra_msg_or_nil, 'Found the not expected %s %s in string %s', + isPattern and 'pattern' or 'substring', sub, str) + end +end + +function M.assertNotStrIContains( str, sub, extra_msg_or_nil ) + -- this relies on lua string.find function + -- a string always contains the empty string + if string.find(str:lower(), sub:lower(), 1, true) then + sub, str = prettystrPairs(sub, str, '\n') + fail_fmt(2, extra_msg_or_nil, 'Found (case insensitively) the not expected substring %s in string %s', + sub, str) + end +end + +function M.assertStrMatches( str, pattern, start, final, extra_msg_or_nil ) + -- Verify a full match for the string + if not strMatch( str, pattern, start, final ) then + pattern, str = prettystrPairs(pattern, str, '\n') + fail_fmt(2, extra_msg_or_nil, 'Could not match pattern %s with string %s', + pattern, str) + end +end + +local function _assertErrorMsgEquals( stripFileAndLine, expectedMsg, func, ... ) + local no_error, error_msg = pcall( func, ... ) + if no_error then + failure( 'No error generated when calling function but expected error: '..M.prettystr(expectedMsg), nil, 3 ) + end + if type(expectedMsg) == "string" and type(error_msg) ~= "string" then + -- table are converted to string automatically + error_msg = tostring(error_msg) + end + local differ = false + if stripFileAndLine then + if error_msg:gsub("^.+:%d+: ", "") ~= expectedMsg then + differ = true + end + else + if error_msg ~= expectedMsg then + local tr = type(error_msg) + local te = type(expectedMsg) + if te == 'table' then + if tr ~= 'table' then + differ = true + else + local ok = pcall(M.assertItemsEquals, error_msg, expectedMsg) + if not ok then + differ = true + end + end + else + differ = true + end + end + end + + if differ then + error_msg, expectedMsg = prettystrPairs(error_msg, expectedMsg) + fail_fmt(3, nil, 'Error message expected: %s\nError message received: %s\n', + expectedMsg, error_msg) + end +end + +function M.assertErrorMsgEquals( expectedMsg, func, ... ) + -- assert that calling f with the arguments will raise an error + -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error + _assertErrorMsgEquals(false, expectedMsg, func, ...) +end + +function M.assertErrorMsgContentEquals(expectedMsg, func, ...) + _assertErrorMsgEquals(true, expectedMsg, func, ...) +end + +function M.assertErrorMsgContains( partialMsg, func, ... ) + -- assert that calling f with the arguments will raise an error + -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error + local no_error, error_msg = pcall( func, ... ) + if no_error then + failure( 'No error generated when calling function but expected error containing: '..prettystr(partialMsg), nil, 2 ) + end + if type(error_msg) ~= "string" then + error_msg = tostring(error_msg) + end + if not string.find( error_msg, partialMsg, nil, true ) then + error_msg, partialMsg = prettystrPairs(error_msg, partialMsg) + fail_fmt(2, nil, 'Error message does not contain: %s\nError message received: %s\n', + partialMsg, error_msg) + end +end + +function M.assertErrorMsgMatches( expectedMsg, func, ... ) + -- assert that calling f with the arguments will raise an error + -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error + local no_error, error_msg = pcall( func, ... ) + if no_error then + failure( 'No error generated when calling function but expected error matching: "'..expectedMsg..'"', nil, 2 ) + end + if type(error_msg) ~= "string" then + error_msg = tostring(error_msg) + end + if not strMatch( error_msg, expectedMsg ) then + expectedMsg, error_msg = prettystrPairs(expectedMsg, error_msg) + fail_fmt(2, nil, 'Error message does not match pattern: %s\nError message received: %s\n', + expectedMsg, error_msg) + end +end + +------------------------------------------------------------------ +-- Type assertions +------------------------------------------------------------------ + +function M.assertEvalToTrue(value, extra_msg_or_nil) + if not value then + failure("expected: a value evaluating to true, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertEvalToFalse(value, extra_msg_or_nil) + if value then + failure("expected: false or nil, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsTrue(value, extra_msg_or_nil) + if value ~= true then + failure("expected: true, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertNotIsTrue(value, extra_msg_or_nil) + if value == true then + failure("expected: not true, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsFalse(value, extra_msg_or_nil) + if value ~= false then + failure("expected: false, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertNotIsFalse(value, extra_msg_or_nil) + if value == false then + failure("expected: not false, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsNil(value, extra_msg_or_nil) + if value ~= nil then + failure("expected: nil, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertNotIsNil(value, extra_msg_or_nil) + if value == nil then + failure("expected: not nil, actual: nil", extra_msg_or_nil, 2) + end +end + +--[[ +Add type assertion functions to the module table M. Each of these functions +takes a single parameter "value", and checks that its Lua type matches the +expected string (derived from the function name): + +M.assertIsXxx(value) -> ensure that type(value) conforms to "xxx" +]] +for _, funcName in ipairs( + {'assertIsNumber', 'assertIsString', 'assertIsTable', 'assertIsBoolean', + 'assertIsFunction', 'assertIsUserdata', 'assertIsThread'} +) do + local typeExpected = funcName:match("^assertIs([A-Z]%a*)$") + -- Lua type() always returns lowercase, also make sure the match() succeeded + typeExpected = typeExpected and typeExpected:lower() + or error("bad function name '"..funcName.."' for type assertion") + + M[funcName] = function(value, extra_msg_or_nil) + if type(value) ~= typeExpected then + if type(value) == 'nil' then + fail_fmt(2, extra_msg_or_nil, 'expected: a %s value, actual: nil', + typeExpected, type(value), prettystrPairs(value)) + else + fail_fmt(2, extra_msg_or_nil, 'expected: a %s value, actual: type %s, value %s', + typeExpected, type(value), prettystrPairs(value)) + end + end + end +end + +--[[ +Add shortcuts for verifying type of a variable, without failure (luaunit v2 compatibility) +M.isXxx(value) -> returns true if type(value) conforms to "xxx" +]] +for _, typeExpected in ipairs( + {'Number', 'String', 'Table', 'Boolean', + 'Function', 'Userdata', 'Thread', 'Nil' } +) do + local typeExpectedLower = typeExpected:lower() + local isType = function(value) + return (type(value) == typeExpectedLower) + end + M['is'..typeExpected] = isType + M['is_'..typeExpectedLower] = isType +end + +--[[ +Add non-type assertion functions to the module table M. Each of these functions +takes a single parameter "value", and checks that its Lua type differs from the +expected string (derived from the function name): + +M.assertNotIsXxx(value) -> ensure that type(value) is not "xxx" +]] +for _, funcName in ipairs( + {'assertNotIsNumber', 'assertNotIsString', 'assertNotIsTable', 'assertNotIsBoolean', + 'assertNotIsFunction', 'assertNotIsUserdata', 'assertNotIsThread'} +) do + local typeUnexpected = funcName:match("^assertNotIs([A-Z]%a*)$") + -- Lua type() always returns lowercase, also make sure the match() succeeded + typeUnexpected = typeUnexpected and typeUnexpected:lower() + or error("bad function name '"..funcName.."' for type assertion") + + M[funcName] = function(value, extra_msg_or_nil) + if type(value) == typeUnexpected then + fail_fmt(2, extra_msg_or_nil, 'expected: not a %s type, actual: value %s', + typeUnexpected, prettystrPairs(value)) + end + end +end + +function M.assertIs(actual, expected, extra_msg_or_nil) + if actual ~= expected then + if not M.ORDER_ACTUAL_EXPECTED then + actual, expected = expected, actual + end + local old_print_table_ref_in_error_msg = M.PRINT_TABLE_REF_IN_ERROR_MSG + M.PRINT_TABLE_REF_IN_ERROR_MSG = true + expected, actual = prettystrPairs(expected, actual, '\n', '') + M.PRINT_TABLE_REF_IN_ERROR_MSG = old_print_table_ref_in_error_msg + fail_fmt(2, extra_msg_or_nil, 'expected and actual object should not be different\nExpected: %s\nReceived: %s', + expected, actual) + end +end + +function M.assertNotIs(actual, expected, extra_msg_or_nil) + if actual == expected then + local old_print_table_ref_in_error_msg = M.PRINT_TABLE_REF_IN_ERROR_MSG + M.PRINT_TABLE_REF_IN_ERROR_MSG = true + local s_expected + if not M.ORDER_ACTUAL_EXPECTED then + s_expected = prettystrPairs(actual) + else + s_expected = prettystrPairs(expected) + end + M.PRINT_TABLE_REF_IN_ERROR_MSG = old_print_table_ref_in_error_msg + fail_fmt(2, extra_msg_or_nil, 'expected and actual object should be different: %s', s_expected ) + end +end + + +------------------------------------------------------------------ +-- Scientific assertions +------------------------------------------------------------------ + + +function M.assertIsNaN(value, extra_msg_or_nil) + if type(value) ~= "number" or value == value then + failure("expected: NaN, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertNotIsNaN(value, extra_msg_or_nil) + if type(value) == "number" and value ~= value then + failure("expected: not NaN, actual: NaN", extra_msg_or_nil, 2) + end +end + +function M.assertIsInf(value, extra_msg_or_nil) + if type(value) ~= "number" or math.abs(value) ~= math.huge then + failure("expected: #Inf, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsPlusInf(value, extra_msg_or_nil) + if type(value) ~= "number" or value ~= math.huge then + failure("expected: #Inf, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsMinusInf(value, extra_msg_or_nil) + if type(value) ~= "number" or value ~= -math.huge then + failure("expected: -#Inf, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertNotIsPlusInf(value, extra_msg_or_nil) + if type(value) == "number" and value == math.huge then + failure("expected: not #Inf, actual: #Inf", extra_msg_or_nil, 2) + end +end + +function M.assertNotIsMinusInf(value, extra_msg_or_nil) + if type(value) == "number" and value == -math.huge then + failure("expected: not -#Inf, actual: -#Inf", extra_msg_or_nil, 2) + end +end + +function M.assertNotIsInf(value, extra_msg_or_nil) + if type(value) == "number" and math.abs(value) == math.huge then + failure("expected: not infinity, actual: " .. prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsPlusZero(value, extra_msg_or_nil) + if type(value) ~= 'number' or value ~= 0 then + failure("expected: +0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2) + else if (1/value == -math.huge) then + -- more precise error diagnosis + failure("expected: +0.0, actual: -0.0", extra_msg_or_nil, 2) + else if (1/value ~= math.huge) then + -- strange, case should have already been covered + failure("expected: +0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end + end + end +end + +function M.assertIsMinusZero(value, extra_msg_or_nil) + if type(value) ~= 'number' or value ~= 0 then + failure("expected: -0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2) + else if (1/value == math.huge) then + -- more precise error diagnosis + failure("expected: -0.0, actual: +0.0", extra_msg_or_nil, 2) + else if (1/value ~= -math.huge) then + -- strange, case should have already been covered + failure("expected: -0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end + end + end +end + +function M.assertNotIsPlusZero(value, extra_msg_or_nil) + if type(value) == 'number' and (1/value == math.huge) then + failure("expected: not +0.0, actual: +0.0", extra_msg_or_nil, 2) + end +end + +function M.assertNotIsMinusZero(value, extra_msg_or_nil) + if type(value) == 'number' and (1/value == -math.huge) then + failure("expected: not -0.0, actual: -0.0", extra_msg_or_nil, 2) + end +end + +function M.assertTableContains(t, expected) + -- checks that table t contains the expected element + if table_findkeyof(t, expected) == nil then + t, expected = prettystrPairs(t, expected) + fail_fmt(2, 'Table %s does NOT contain the expected element %s', + t, expected) + end +end + +function M.assertNotTableContains(t, expected) + -- checks that table t doesn't contain the expected element + local k = table_findkeyof(t, expected) + if k ~= nil then + t, expected = prettystrPairs(t, expected) + fail_fmt(2, 'Table %s DOES contain the unwanted element %s (at key %s)', + t, expected, prettystr(k)) + end +end + +---------------------------------------------------------------- +-- Compatibility layer +---------------------------------------------------------------- + +-- for compatibility with LuaUnit v2.x +function M.wrapFunctions() + -- In LuaUnit version <= 2.1 , this function was necessary to include + -- a test function inside the global test suite. Nowadays, the functions + -- are simply run directly as part of the test discovery process. + -- so just do nothing ! + io.stderr:write[[Use of WrapFunctions() is no longer needed. +Just prefix your test function names with "test" or "Test" and they +will be picked up and run by LuaUnit. +]] +end + +local list_of_funcs = { + -- { official function name , alias } + + -- general assertions + { 'assertEquals' , 'assert_equals' }, + { 'assertItemsEquals' , 'assert_items_equals' }, + { 'assertNotEquals' , 'assert_not_equals' }, + { 'assertAlmostEquals' , 'assert_almost_equals' }, + { 'assertNotAlmostEquals' , 'assert_not_almost_equals' }, + { 'assertEvalToTrue' , 'assert_eval_to_true' }, + { 'assertEvalToFalse' , 'assert_eval_to_false' }, + { 'assertStrContains' , 'assert_str_contains' }, + { 'assertStrIContains' , 'assert_str_icontains' }, + { 'assertNotStrContains' , 'assert_not_str_contains' }, + { 'assertNotStrIContains' , 'assert_not_str_icontains' }, + { 'assertStrMatches' , 'assert_str_matches' }, + { 'assertError' , 'assert_error' }, + { 'assertErrorMsgEquals' , 'assert_error_msg_equals' }, + { 'assertErrorMsgContains' , 'assert_error_msg_contains' }, + { 'assertErrorMsgMatches' , 'assert_error_msg_matches' }, + { 'assertErrorMsgContentEquals', 'assert_error_msg_content_equals' }, + { 'assertIs' , 'assert_is' }, + { 'assertNotIs' , 'assert_not_is' }, + { 'assertTableContains' , 'assert_table_contains' }, + { 'assertNotTableContains' , 'assert_not_table_contains' }, + { 'wrapFunctions' , 'WrapFunctions' }, + { 'wrapFunctions' , 'wrap_functions' }, + + -- type assertions: assertIsXXX -> assert_is_xxx + { 'assertIsNumber' , 'assert_is_number' }, + { 'assertIsString' , 'assert_is_string' }, + { 'assertIsTable' , 'assert_is_table' }, + { 'assertIsBoolean' , 'assert_is_boolean' }, + { 'assertIsNil' , 'assert_is_nil' }, + { 'assertIsTrue' , 'assert_is_true' }, + { 'assertIsFalse' , 'assert_is_false' }, + { 'assertIsNaN' , 'assert_is_nan' }, + { 'assertIsInf' , 'assert_is_inf' }, + { 'assertIsPlusInf' , 'assert_is_plus_inf' }, + { 'assertIsMinusInf' , 'assert_is_minus_inf' }, + { 'assertIsPlusZero' , 'assert_is_plus_zero' }, + { 'assertIsMinusZero' , 'assert_is_minus_zero' }, + { 'assertIsFunction' , 'assert_is_function' }, + { 'assertIsThread' , 'assert_is_thread' }, + { 'assertIsUserdata' , 'assert_is_userdata' }, + + -- type assertions: assertIsXXX -> assertXxx + { 'assertIsNumber' , 'assertNumber' }, + { 'assertIsString' , 'assertString' }, + { 'assertIsTable' , 'assertTable' }, + { 'assertIsBoolean' , 'assertBoolean' }, + { 'assertIsNil' , 'assertNil' }, + { 'assertIsTrue' , 'assertTrue' }, + { 'assertIsFalse' , 'assertFalse' }, + { 'assertIsNaN' , 'assertNaN' }, + { 'assertIsInf' , 'assertInf' }, + { 'assertIsPlusInf' , 'assertPlusInf' }, + { 'assertIsMinusInf' , 'assertMinusInf' }, + { 'assertIsPlusZero' , 'assertPlusZero' }, + { 'assertIsMinusZero' , 'assertMinusZero'}, + { 'assertIsFunction' , 'assertFunction' }, + { 'assertIsThread' , 'assertThread' }, + { 'assertIsUserdata' , 'assertUserdata' }, + + -- type assertions: assertIsXXX -> assert_xxx (luaunit v2 compat) + { 'assertIsNumber' , 'assert_number' }, + { 'assertIsString' , 'assert_string' }, + { 'assertIsTable' , 'assert_table' }, + { 'assertIsBoolean' , 'assert_boolean' }, + { 'assertIsNil' , 'assert_nil' }, + { 'assertIsTrue' , 'assert_true' }, + { 'assertIsFalse' , 'assert_false' }, + { 'assertIsNaN' , 'assert_nan' }, + { 'assertIsInf' , 'assert_inf' }, + { 'assertIsPlusInf' , 'assert_plus_inf' }, + { 'assertIsMinusInf' , 'assert_minus_inf' }, + { 'assertIsPlusZero' , 'assert_plus_zero' }, + { 'assertIsMinusZero' , 'assert_minus_zero' }, + { 'assertIsFunction' , 'assert_function' }, + { 'assertIsThread' , 'assert_thread' }, + { 'assertIsUserdata' , 'assert_userdata' }, + + -- type assertions: assertNotIsXXX -> assert_not_is_xxx + { 'assertNotIsNumber' , 'assert_not_is_number' }, + { 'assertNotIsString' , 'assert_not_is_string' }, + { 'assertNotIsTable' , 'assert_not_is_table' }, + { 'assertNotIsBoolean' , 'assert_not_is_boolean' }, + { 'assertNotIsNil' , 'assert_not_is_nil' }, + { 'assertNotIsTrue' , 'assert_not_is_true' }, + { 'assertNotIsFalse' , 'assert_not_is_false' }, + { 'assertNotIsNaN' , 'assert_not_is_nan' }, + { 'assertNotIsInf' , 'assert_not_is_inf' }, + { 'assertNotIsPlusInf' , 'assert_not_plus_inf' }, + { 'assertNotIsMinusInf' , 'assert_not_minus_inf' }, + { 'assertNotIsPlusZero' , 'assert_not_plus_zero' }, + { 'assertNotIsMinusZero' , 'assert_not_minus_zero' }, + { 'assertNotIsFunction' , 'assert_not_is_function' }, + { 'assertNotIsThread' , 'assert_not_is_thread' }, + { 'assertNotIsUserdata' , 'assert_not_is_userdata' }, + + -- type assertions: assertNotIsXXX -> assertNotXxx (luaunit v2 compat) + { 'assertNotIsNumber' , 'assertNotNumber' }, + { 'assertNotIsString' , 'assertNotString' }, + { 'assertNotIsTable' , 'assertNotTable' }, + { 'assertNotIsBoolean' , 'assertNotBoolean' }, + { 'assertNotIsNil' , 'assertNotNil' }, + { 'assertNotIsTrue' , 'assertNotTrue' }, + { 'assertNotIsFalse' , 'assertNotFalse' }, + { 'assertNotIsNaN' , 'assertNotNaN' }, + { 'assertNotIsInf' , 'assertNotInf' }, + { 'assertNotIsPlusInf' , 'assertNotPlusInf' }, + { 'assertNotIsMinusInf' , 'assertNotMinusInf' }, + { 'assertNotIsPlusZero' , 'assertNotPlusZero' }, + { 'assertNotIsMinusZero' , 'assertNotMinusZero' }, + { 'assertNotIsFunction' , 'assertNotFunction' }, + { 'assertNotIsThread' , 'assertNotThread' }, + { 'assertNotIsUserdata' , 'assertNotUserdata' }, + + -- type assertions: assertNotIsXXX -> assert_not_xxx + { 'assertNotIsNumber' , 'assert_not_number' }, + { 'assertNotIsString' , 'assert_not_string' }, + { 'assertNotIsTable' , 'assert_not_table' }, + { 'assertNotIsBoolean' , 'assert_not_boolean' }, + { 'assertNotIsNil' , 'assert_not_nil' }, + { 'assertNotIsTrue' , 'assert_not_true' }, + { 'assertNotIsFalse' , 'assert_not_false' }, + { 'assertNotIsNaN' , 'assert_not_nan' }, + { 'assertNotIsInf' , 'assert_not_inf' }, + { 'assertNotIsPlusInf' , 'assert_not_plus_inf' }, + { 'assertNotIsMinusInf' , 'assert_not_minus_inf' }, + { 'assertNotIsPlusZero' , 'assert_not_plus_zero' }, + { 'assertNotIsMinusZero' , 'assert_not_minus_zero' }, + { 'assertNotIsFunction' , 'assert_not_function' }, + { 'assertNotIsThread' , 'assert_not_thread' }, + { 'assertNotIsUserdata' , 'assert_not_userdata' }, + + -- all assertions with Coroutine duplicate Thread assertions + { 'assertIsThread' , 'assertIsCoroutine' }, + { 'assertIsThread' , 'assertCoroutine' }, + { 'assertIsThread' , 'assert_is_coroutine' }, + { 'assertIsThread' , 'assert_coroutine' }, + { 'assertNotIsThread' , 'assertNotIsCoroutine' }, + { 'assertNotIsThread' , 'assertNotCoroutine' }, + { 'assertNotIsThread' , 'assert_not_is_coroutine' }, + { 'assertNotIsThread' , 'assert_not_coroutine' }, +} + +-- Create all aliases in M +for _,v in ipairs( list_of_funcs ) do + local funcname, alias = v[1], v[2] + M[alias] = M[funcname] + + if EXPORT_ASSERT_TO_GLOBALS then + _G[funcname] = M[funcname] + _G[alias] = M[funcname] + end +end + +---------------------------------------------------------------- +-- +-- Outputters +-- +---------------------------------------------------------------- + +-- A common "base" class for outputters +-- For concepts involved (class inheritance) see http://www.lua.org/pil/16.2.html + +local genericOutput = { __class__ = 'genericOutput' } -- class +local genericOutput_MT = { __index = genericOutput } -- metatable +M.genericOutput = genericOutput -- publish, so that custom classes may derive from it + +function genericOutput.new(runner, default_verbosity) + -- runner is the "parent" object controlling the output, usually a LuaUnit instance + local t = { runner = runner } + if runner then + t.result = runner.result + t.verbosity = runner.verbosity or default_verbosity + t.fname = runner.fname + else + t.verbosity = default_verbosity + end + return setmetatable( t, genericOutput_MT) +end + +-- abstract ("empty") methods +function genericOutput:startSuite() + -- Called once, when the suite is started +end + +function genericOutput:startClass(className) + -- Called each time a new test class is started +end + +function genericOutput:startTest(testName) + -- called each time a new test is started, right before the setUp() + -- the current test status node is already created and available in: self.result.currentNode +end + +function genericOutput:updateStatus(node) + -- called with status failed or error as soon as the error/failure is encountered + -- this method is NOT called for a successful test because a test is marked as successful by default + -- and does not need to be updated +end + +function genericOutput:endTest(node) + -- called when the test is finished, after the tearDown() method +end + +function genericOutput:endClass() + -- called when executing the class is finished, before moving on to the next class of at the end of the test execution +end + +function genericOutput:endSuite() + -- called at the end of the test suite execution +end + + +---------------------------------------------------------------- +-- class TapOutput +---------------------------------------------------------------- + +local TapOutput = genericOutput.new() -- derived class +local TapOutput_MT = { __index = TapOutput } -- metatable +TapOutput.__class__ = 'TapOutput' + + -- For a good reference for TAP format, check: http://testanything.org/tap-specification.html + + function TapOutput.new(runner) + local t = genericOutput.new(runner, M.VERBOSITY_LOW) + return setmetatable( t, TapOutput_MT) + end + function TapOutput:startSuite() + print("1.."..self.result.selectedCount) + print('# Started on '..self.result.startDate) + end + function TapOutput:startClass(className) + if className ~= '[TestFunctions]' then + print('# Starting class: '..className) + end + end + + function TapOutput:updateStatus( node ) + if node:isSkipped() then + io.stdout:write("ok ", self.result.currentTestNumber, "\t# SKIP ", node.msg, "\n" ) + return + end + + io.stdout:write("not ok ", self.result.currentTestNumber, "\t", node.testName, "\n") + if self.verbosity > M.VERBOSITY_LOW then + print( prefixString( '# ', node.msg ) ) + end + if (node:isFailure() or node:isError()) and self.verbosity > M.VERBOSITY_DEFAULT then + print( prefixString( '# ', node.stackTrace ) ) + end + end + + function TapOutput:endTest( node ) + if node:isSuccess() then + io.stdout:write("ok ", self.result.currentTestNumber, "\t", node.testName, "\n") + end + end + + function TapOutput:endSuite() + print( '# '..M.LuaUnit.statusLine( self.result ) ) + return self.result.notSuccessCount + end + + +-- class TapOutput end + +---------------------------------------------------------------- +-- class JUnitOutput +---------------------------------------------------------------- + +-- See directory junitxml for more information about the junit format +local JUnitOutput = genericOutput.new() -- derived class +local JUnitOutput_MT = { __index = JUnitOutput } -- metatable +JUnitOutput.__class__ = 'JUnitOutput' + + function JUnitOutput.new(runner) + local t = genericOutput.new(runner, M.VERBOSITY_LOW) + t.testList = {} + return setmetatable( t, JUnitOutput_MT ) + end + + function JUnitOutput:startSuite() + -- open xml file early to deal with errors + if self.fname == nil then + error('With Junit, an output filename must be supplied with --name!') + end + if string.sub(self.fname,-4) ~= '.xml' then + self.fname = self.fname..'.xml' + end + self.fd = io.open(self.fname, "w") + if self.fd == nil then + error("Could not open file for writing: "..self.fname) + end + + print('# XML output to '..self.fname) + print('# Started on '..self.result.startDate) + end + function JUnitOutput:startClass(className) + if className ~= '[TestFunctions]' then + print('# Starting class: '..className) + end + end + function JUnitOutput:startTest(testName) + print('# Starting test: '..testName) + end + + function JUnitOutput:updateStatus( node ) + if node:isFailure() then + print( '# Failure: ' .. prefixString( '# ', node.msg ):sub(4, nil) ) + -- print('# ' .. node.stackTrace) + elseif node:isError() then + print( '# Error: ' .. prefixString( '# ' , node.msg ):sub(4, nil) ) + -- print('# ' .. node.stackTrace) + end + end + + function JUnitOutput:endSuite() + print( '# '..M.LuaUnit.statusLine(self.result)) + + -- XML file writing + self.fd:write('\n') + self.fd:write('\n') + self.fd:write(string.format( + ' \n', + self.result.runCount, self.result.startIsodate, self.result.duration, self.result.errorCount, self.result.failureCount, self.result.skippedCount )) + self.fd:write(" \n") + self.fd:write(string.format(' \n', _VERSION ) ) + self.fd:write(string.format(' \n', M.VERSION) ) + -- XXX please include system name and version if possible + self.fd:write(" \n") + + for i,node in ipairs(self.result.allTests) do + self.fd:write(string.format(' \n', + node.className, node.testName, node.duration ) ) + if node:isNotSuccess() then + self.fd:write(node:statusXML()) + end + self.fd:write(' \n') + end + + -- Next two lines are needed to validate junit ANT xsd, but really not useful in general: + self.fd:write(' \n') + self.fd:write(' \n') + + self.fd:write(' \n') + self.fd:write('\n') + self.fd:close() + return self.result.notSuccessCount + end + + +-- class TapOutput end + +---------------------------------------------------------------- +-- class TextOutput +---------------------------------------------------------------- + +--[[ Example of other unit-tests suite text output + +-- Python Non verbose: + +For each test: . or F or E + +If some failed tests: + ============== + ERROR / FAILURE: TestName (testfile.testclass) + --------- + Stack trace + + +then -------------- +then "Ran x tests in 0.000s" +then OK or FAILED (failures=1, error=1) + +-- Python Verbose: +testname (filename.classname) ... ok +testname (filename.classname) ... FAIL +testname (filename.classname) ... ERROR + +then -------------- +then "Ran x tests in 0.000s" +then OK or FAILED (failures=1, error=1) + +-- Ruby: +Started + . + Finished in 0.002695 seconds. + + 1 tests, 2 assertions, 0 failures, 0 errors + +-- Ruby: +>> ruby tc_simple_number2.rb +Loaded suite tc_simple_number2 +Started +F.. +Finished in 0.038617 seconds. + + 1) Failure: +test_failure(TestSimpleNumber) [tc_simple_number2.rb:16]: +Adding doesn't work. +<3> expected but was +<4>. + +3 tests, 4 assertions, 1 failures, 0 errors + +-- Java Junit +.......F. +Time: 0,003 +There was 1 failure: +1) testCapacity(junit.samples.VectorTest)junit.framework.AssertionFailedError + at junit.samples.VectorTest.testCapacity(VectorTest.java:87) + at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) + at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) + at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) + +FAILURES!!! +Tests run: 8, Failures: 1, Errors: 0 + + +-- Maven + +# mvn test +------------------------------------------------------- + T E S T S +------------------------------------------------------- +Running math.AdditionTest +Tests run: 2, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: +0.03 sec <<< FAILURE! + +Results : + +Failed tests: + testLireSymbole(math.AdditionTest) + +Tests run: 2, Failures: 1, Errors: 0, Skipped: 0 + + +-- LuaUnit +---- non verbose +* display . or F or E when running tests +---- verbose +* display test name + ok/fail +---- +* blank line +* number) ERROR or FAILURE: TestName + Stack trace +* blank line +* number) ERROR or FAILURE: TestName + Stack trace + +then -------------- +then "Ran x tests in 0.000s (%d not selected, %d skipped)" +then OK or FAILED (failures=1, error=1) + + +]] + +local TextOutput = genericOutput.new() -- derived class +local TextOutput_MT = { __index = TextOutput } -- metatable +TextOutput.__class__ = 'TextOutput' + + function TextOutput.new(runner) + local t = genericOutput.new(runner, M.VERBOSITY_DEFAULT) + t.errorList = {} + return setmetatable( t, TextOutput_MT ) + end + + function TextOutput:startSuite() + if self.verbosity > M.VERBOSITY_DEFAULT then + print( 'Started on '.. self.result.startDate ) + end + end + + function TextOutput:startTest(testName) + if self.verbosity > M.VERBOSITY_DEFAULT then + io.stdout:write( " ", self.result.currentNode.testName, " ... " ) + end + end + + function TextOutput:endTest( node ) + if node:isSuccess() then + if self.verbosity > M.VERBOSITY_DEFAULT then + io.stdout:write("Ok\n") + else + io.stdout:write(".") + io.stdout:flush() + end + else + if self.verbosity > M.VERBOSITY_DEFAULT then + print( node.status ) + print( node.msg ) + --[[ + -- find out when to do this: + if self.verbosity > M.VERBOSITY_DEFAULT then + print( node.stackTrace ) + end + ]] + else + -- write only the first character of status E, F or S + io.stdout:write(string.sub(node.status, 1, 1)) + io.stdout:flush() + end + end + end + + function TextOutput:displayOneFailedTest( index, fail ) + print(index..") "..fail.testName ) + print( fail.msg ) + print( fail.stackTrace ) + print() + end + + function TextOutput:displayErroredTests() + if #self.result.errorTests ~= 0 then + print("Tests with errors:") + print("------------------") + for i, v in ipairs(self.result.errorTests) do + self:displayOneFailedTest(i, v) + end + end + end + + function TextOutput:displayFailedTests() + if #self.result.failedTests ~= 0 then + print("Failed tests:") + print("-------------") + for i, v in ipairs(self.result.failedTests) do + self:displayOneFailedTest(i, v) + end + end + end + + function TextOutput:endSuite() + if self.verbosity > M.VERBOSITY_DEFAULT then + print("=========================================================") + else + print() + end + self:displayErroredTests() + self:displayFailedTests() + print( M.LuaUnit.statusLine( self.result ) ) + if self.result.notSuccessCount == 0 then + print('OK') + end + end + +-- class TextOutput end + + +---------------------------------------------------------------- +-- class NilOutput +---------------------------------------------------------------- + +local function nopCallable() + --print(42) + return nopCallable +end + +local NilOutput = { __class__ = 'NilOuptut' } -- class +local NilOutput_MT = { __index = nopCallable } -- metatable + +function NilOutput.new(runner) + return setmetatable( { __class__ = 'NilOutput' }, NilOutput_MT ) +end + +---------------------------------------------------------------- +-- +-- class LuaUnit +-- +---------------------------------------------------------------- + +M.LuaUnit = { + outputType = TextOutput, + verbosity = M.VERBOSITY_DEFAULT, + __class__ = 'LuaUnit' +} +local LuaUnit_MT = { __index = M.LuaUnit } + +if EXPORT_ASSERT_TO_GLOBALS then + LuaUnit = M.LuaUnit +end + + function M.LuaUnit.new() + return setmetatable( {}, LuaUnit_MT ) + end + + -----------------[[ Utility methods ]]--------------------- + + function M.LuaUnit.asFunction(aObject) + -- return "aObject" if it is a function, and nil otherwise + if 'function' == type(aObject) then + return aObject + end + end + + function M.LuaUnit.splitClassMethod(someName) + --[[ + Return a pair of className, methodName strings for a name in the form + "class.method". If no class part (or separator) is found, will return + nil, someName instead (the latter being unchanged). + + This convention thus also replaces the older isClassMethod() test: + You just have to check for a non-nil className (return) value. + ]] + local separator = string.find(someName, '.', 1, true) + if separator then + return someName:sub(1, separator - 1), someName:sub(separator + 1) + end + return nil, someName + end + + function M.LuaUnit.isMethodTestName( s ) + -- return true is the name matches the name of a test method + -- default rule is that is starts with 'Test' or with 'test' + return string.sub(s, 1, 4):lower() == 'test' + end + + function M.LuaUnit.isTestName( s ) + -- return true is the name matches the name of a test + -- default rule is that is starts with 'Test' or with 'test' + return string.sub(s, 1, 4):lower() == 'test' + end + + function M.LuaUnit.collectTests() + -- return a list of all test names in the global namespace + -- that match LuaUnit.isTestName + + local testNames = {} + for k, _ in pairs(_G) do + if type(k) == "string" and M.LuaUnit.isTestName( k ) then + table.insert( testNames , k ) + end + end + table.sort( testNames ) + return testNames + end + + function M.LuaUnit.parseCmdLine( cmdLine ) + -- parse the command line + -- Supported command line parameters: + -- --verbose, -v: increase verbosity + -- --quiet, -q: silence output + -- --error, -e: treat errors as fatal (quit program) + -- --output, -o, + name: select output type + -- --pattern, -p, + pattern: run test matching pattern, may be repeated + -- --exclude, -x, + pattern: run test not matching pattern, may be repeated + -- --shuffle, -s, : shuffle tests before reunning them + -- --name, -n, + fname: name of output file for junit, default to stdout + -- --repeat, -r, + num: number of times to execute each test + -- [testnames, ...]: run selected test names + -- + -- Returns a table with the following fields: + -- verbosity: nil, M.VERBOSITY_DEFAULT, M.VERBOSITY_QUIET, M.VERBOSITY_VERBOSE + -- output: nil, 'tap', 'junit', 'text', 'nil' + -- testNames: nil or a list of test names to run + -- exeRepeat: num or 1 + -- pattern: nil or a list of patterns + -- exclude: nil or a list of patterns + + local result, state = {}, nil + local SET_OUTPUT = 1 + local SET_PATTERN = 2 + local SET_EXCLUDE = 3 + local SET_FNAME = 4 + local SET_REPEAT = 5 + + if cmdLine == nil then + return result + end + + local function parseOption( option ) + if option == '--help' or option == '-h' then + result['help'] = true + return + elseif option == '--version' then + result['version'] = true + return + elseif option == '--verbose' or option == '-v' then + result['verbosity'] = M.VERBOSITY_VERBOSE + return + elseif option == '--quiet' or option == '-q' then + result['verbosity'] = M.VERBOSITY_QUIET + return + elseif option == '--error' or option == '-e' then + result['quitOnError'] = true + return + elseif option == '--failure' or option == '-f' then + result['quitOnFailure'] = true + return + elseif option == '--shuffle' or option == '-s' then + result['shuffle'] = true + return + elseif option == '--output' or option == '-o' then + state = SET_OUTPUT + return state + elseif option == '--name' or option == '-n' then + state = SET_FNAME + return state + elseif option == '--repeat' or option == '-r' then + state = SET_REPEAT + return state + elseif option == '--pattern' or option == '-p' then + state = SET_PATTERN + return state + elseif option == '--exclude' or option == '-x' then + state = SET_EXCLUDE + return state + end + error('Unknown option: '..option,3) + end + + local function setArg( cmdArg, state ) + if state == SET_OUTPUT then + result['output'] = cmdArg + return + elseif state == SET_FNAME then + result['fname'] = cmdArg + return + elseif state == SET_REPEAT then + result['exeRepeat'] = tonumber(cmdArg) + or error('Malformed -r argument: '..cmdArg) + return + elseif state == SET_PATTERN then + if result['pattern'] then + table.insert( result['pattern'], cmdArg ) + else + result['pattern'] = { cmdArg } + end + return + elseif state == SET_EXCLUDE then + local notArg = '!'..cmdArg + if result['pattern'] then + table.insert( result['pattern'], notArg ) + else + result['pattern'] = { notArg } + end + return + end + error('Unknown parse state: '.. state) + end + + + for i, cmdArg in ipairs(cmdLine) do + if state ~= nil then + setArg( cmdArg, state, result ) + state = nil + else + if cmdArg:sub(1,1) == '-' then + state = parseOption( cmdArg ) + else + if result['testNames'] then + table.insert( result['testNames'], cmdArg ) + else + result['testNames'] = { cmdArg } + end + end + end + end + + if result['help'] then + M.LuaUnit.help() + end + + if result['version'] then + M.LuaUnit.version() + end + + if state ~= nil then + error('Missing argument after '..cmdLine[ #cmdLine ],2 ) + end + + return result + end + + function M.LuaUnit.help() + print(M.USAGE) + os.exit(0) + end + + function M.LuaUnit.version() + print('LuaUnit v'..M.VERSION..' by Philippe Fremy ') + os.exit(0) + end + +---------------------------------------------------------------- +-- class NodeStatus +---------------------------------------------------------------- + + local NodeStatus = { __class__ = 'NodeStatus' } -- class + local NodeStatus_MT = { __index = NodeStatus } -- metatable + M.NodeStatus = NodeStatus + + -- values of status + NodeStatus.SUCCESS = 'SUCCESS' + NodeStatus.SKIP = 'SKIP' + NodeStatus.FAIL = 'FAIL' + NodeStatus.ERROR = 'ERROR' + + function NodeStatus.new( number, testName, className ) + -- default constructor, test are PASS by default + local t = { number = number, testName = testName, className = className } + setmetatable( t, NodeStatus_MT ) + t:success() + return t + end + + function NodeStatus:success() + self.status = self.SUCCESS + -- useless because lua does this for us, but it helps me remembering the relevant field names + self.msg = nil + self.stackTrace = nil + end + + function NodeStatus:skip(msg) + self.status = self.SKIP + self.msg = msg + self.stackTrace = nil + end + + function NodeStatus:fail(msg, stackTrace) + self.status = self.FAIL + self.msg = msg + self.stackTrace = stackTrace + end + + function NodeStatus:error(msg, stackTrace) + self.status = self.ERROR + self.msg = msg + self.stackTrace = stackTrace + end + + function NodeStatus:isSuccess() + return self.status == NodeStatus.SUCCESS + end + + function NodeStatus:isNotSuccess() + -- Return true if node is either failure or error or skip + return (self.status == NodeStatus.FAIL or self.status == NodeStatus.ERROR or self.status == NodeStatus.SKIP) + end + + function NodeStatus:isSkipped() + return self.status == NodeStatus.SKIP + end + + function NodeStatus:isFailure() + return self.status == NodeStatus.FAIL + end + + function NodeStatus:isError() + return self.status == NodeStatus.ERROR + end + + function NodeStatus:statusXML() + if self:isError() then + return table.concat( + {' \n', + ' \n'}) + elseif self:isFailure() then + return table.concat( + {' \n', + ' \n'}) + elseif self:isSkipped() then + return table.concat({' ', xmlEscape(self.msg),'\n' } ) + end + return ' \n' -- (not XSD-compliant! normally shouldn't get here) + end + + --------------[[ Output methods ]]------------------------- + + local function conditional_plural(number, singular) + -- returns a grammatically well-formed string "%d " + local suffix = '' + if number ~= 1 then -- use plural + suffix = (singular:sub(-2) == 'ss') and 'es' or 's' + end + return string.format('%d %s%s', number, singular, suffix) + end + + function M.LuaUnit.statusLine(result) + -- return status line string according to results + local s = { + string.format('Ran %d tests in %0.3f seconds', + result.runCount, result.duration), + conditional_plural(result.successCount, 'success'), + } + if result.notSuccessCount > 0 then + if result.failureCount > 0 then + table.insert(s, conditional_plural(result.failureCount, 'failure')) + end + if result.errorCount > 0 then + table.insert(s, conditional_plural(result.errorCount, 'error')) + end + else + table.insert(s, '0 failures') + end + if result.skippedCount > 0 then + table.insert(s, string.format("%d skipped", result.skippedCount)) + end + if result.nonSelectedCount > 0 then + table.insert(s, string.format("%d non-selected", result.nonSelectedCount)) + end + return table.concat(s, ', ') + end + + function M.LuaUnit:startSuite(selectedCount, nonSelectedCount) + self.result = { + selectedCount = selectedCount, + nonSelectedCount = nonSelectedCount, + successCount = 0, + runCount = 0, + currentTestNumber = 0, + currentClassName = "", + currentNode = nil, + suiteStarted = true, + startTime = os.clock(), + startDate = os.date(os.getenv('LUAUNIT_DATEFMT')), + startIsodate = os.date('%Y-%m-%dT%H:%M:%S'), + patternIncludeFilter = self.patternIncludeFilter, + + -- list of test node status + allTests = {}, + failedTests = {}, + errorTests = {}, + skippedTests = {}, + + failureCount = 0, + errorCount = 0, + notSuccessCount = 0, + skippedCount = 0, + } + + self.outputType = self.outputType or TextOutput + self.output = self.outputType.new(self) + self.output:startSuite() + end + + function M.LuaUnit:startClass( className ) + self.result.currentClassName = className + self.output:startClass( className ) + end + + function M.LuaUnit:startTest( testName ) + self.result.currentTestNumber = self.result.currentTestNumber + 1 + self.result.runCount = self.result.runCount + 1 + self.result.currentNode = NodeStatus.new( + self.result.currentTestNumber, + testName, + self.result.currentClassName + ) + self.result.currentNode.startTime = os.clock() + table.insert( self.result.allTests, self.result.currentNode ) + self.output:startTest( testName ) + end + + function M.LuaUnit:updateStatus( err ) + -- "err" is expected to be a table / result from protectedCall() + if err.status == NodeStatus.SUCCESS then + return + end + + local node = self.result.currentNode + + --[[ As a first approach, we will report only one error or one failure for one test. + + However, we can have the case where the test is in failure, and the teardown is in error. + In such case, it's a good idea to report both a failure and an error in the test suite. This is + what Python unittest does for example. However, it mixes up counts so need to be handled carefully: for + example, there could be more (failures + errors) count that tests. What happens to the current node ? + + We will do this more intelligent version later. + ]] + + -- if the node is already in failure/error, just don't report the new error (see above) + if node.status ~= NodeStatus.SUCCESS then + return + end + + if err.status == NodeStatus.FAIL then + node:fail( err.msg, err.trace ) + table.insert( self.result.failedTests, node ) + elseif err.status == NodeStatus.ERROR then + node:error( err.msg, err.trace ) + table.insert( self.result.errorTests, node ) + elseif err.status == NodeStatus.SKIP then + node:skip( err.msg ) + table.insert( self.result.skippedTests, node ) + else + error('No such status: ' .. prettystr(err.status)) + end + + self.output:updateStatus( node ) + end + + function M.LuaUnit:endTest() + local node = self.result.currentNode + -- print( 'endTest() '..prettystr(node)) + -- print( 'endTest() '..prettystr(node:isNotSuccess())) + node.duration = os.clock() - node.startTime + node.startTime = nil + self.output:endTest( node ) + + if node:isSuccess() then + self.result.successCount = self.result.successCount + 1 + elseif node:isError() then + if self.quitOnError or self.quitOnFailure then + -- Runtime error - abort test execution as requested by + -- "--error" option. This is done by setting a special + -- flag that gets handled in runSuiteByInstances(). + print("\nERROR during LuaUnit test execution:\n" .. node.msg) + self.result.aborted = true + end + elseif node:isFailure() then + if self.quitOnFailure then + -- Failure - abort test execution as requested by + -- "--failure" option. This is done by setting a special + -- flag that gets handled in runSuiteByInstances(). + print("\nFailure during LuaUnit test execution:\n" .. node.msg) + self.result.aborted = true + end + elseif node:isSkipped() then + self.result.runCount = self.result.runCount - 1 + else + error('No such node status: ' .. prettystr(node.status)) + end + self.result.currentNode = nil + end + + function M.LuaUnit:endClass() + self.output:endClass() + end + + function M.LuaUnit:endSuite() + if self.result.suiteStarted == false then + error('LuaUnit:endSuite() -- suite was already ended' ) + end + self.result.duration = os.clock()-self.result.startTime + self.result.suiteStarted = false + + -- Expose test counts for outputter's endSuite(). This could be managed + -- internally instead by using the length of the lists of failed tests + -- but unit tests rely on these fields being present. + self.result.failureCount = #self.result.failedTests + self.result.errorCount = #self.result.errorTests + self.result.notSuccessCount = self.result.failureCount + self.result.errorCount + self.result.skippedCount = #self.result.skippedTests + + self.output:endSuite() + end + + function M.LuaUnit:setOutputType(outputType, fname) + -- Configures LuaUnit runner output + -- outputType is one of: NIL, TAP, JUNIT, TEXT + -- when outputType is junit, the additional argument fname is used to set the name of junit output file + -- for other formats, fname is ignored + if outputType:upper() == "NIL" then + self.outputType = NilOutput + return + end + if outputType:upper() == "TAP" then + self.outputType = TapOutput + return + end + if outputType:upper() == "JUNIT" then + self.outputType = JUnitOutput + if fname then + self.fname = fname + end + return + end + if outputType:upper() == "TEXT" then + self.outputType = TextOutput + return + end + error( 'No such format: '..outputType,2) + end + + --------------[[ Runner ]]----------------- + + function M.LuaUnit:protectedCall(classInstance, methodInstance, prettyFuncName) + -- if classInstance is nil, this is just a function call + -- else, it's method of a class being called. + + local function err_handler(e) + -- transform error into a table, adding the traceback information + return { + status = NodeStatus.ERROR, + msg = e, + trace = string.sub(debug.traceback("", 1), 2) + } + end + + local ok, err + if classInstance then + -- stupid Lua < 5.2 does not allow xpcall with arguments so let's use a workaround + ok, err = xpcall( function () methodInstance(classInstance) end, err_handler ) + else + ok, err = xpcall( function () methodInstance() end, err_handler ) + end + if ok then + return {status = NodeStatus.SUCCESS} + end + -- print('ok="'..prettystr(ok)..'" err="'..prettystr(err)..'"') + + local iter_msg + iter_msg = self.exeRepeat and 'iteration '..self.currentCount + + err.msg, err.status = M.adjust_err_msg_with_iter( err.msg, iter_msg ) + + if err.status == NodeStatus.SUCCESS or err.status == NodeStatus.SKIP then + err.trace = nil + return err + end + + -- reformat / improve the stack trace + if prettyFuncName then -- we do have the real method name + err.trace = err.trace:gsub("in (%a+) 'methodInstance'", "in %1 '"..prettyFuncName.."'") + end + if STRIP_LUAUNIT_FROM_STACKTRACE then + err.trace = stripLuaunitTrace2(err.trace, err.msg) + end + + return err -- return the error "object" (table) + end + + + function M.LuaUnit:execOneFunction(className, methodName, classInstance, methodInstance) + -- When executing a test function, className and classInstance must be nil + -- When executing a class method, all parameters must be set + + if type(methodInstance) ~= 'function' then + error( tostring(methodName)..' must be a function, not '..type(methodInstance)) + end + + local prettyFuncName + if className == nil then + className = '[TestFunctions]' + prettyFuncName = methodName + else + prettyFuncName = className..'.'..methodName + end + + if self.lastClassName ~= className then + if self.lastClassName ~= nil then + self:endClass() + end + self:startClass( className ) + self.lastClassName = className + end + + self:startTest(prettyFuncName) + + local node = self.result.currentNode + for iter_n = 1, self.exeRepeat or 1 do + if node:isNotSuccess() then + break + end + self.currentCount = iter_n + + -- run setUp first (if any) + if classInstance then + local func = self.asFunction( classInstance.setUp ) or + self.asFunction( classInstance.Setup ) or + self.asFunction( classInstance.setup ) or + self.asFunction( classInstance.SetUp ) + if func then + self:updateStatus(self:protectedCall(classInstance, func, className..'.setUp')) + end + end + + -- run testMethod() + if node:isSuccess() then + self:updateStatus(self:protectedCall(classInstance, methodInstance, prettyFuncName)) + end + + -- lastly, run tearDown (if any) + if classInstance then + local func = self.asFunction( classInstance.tearDown ) or + self.asFunction( classInstance.TearDown ) or + self.asFunction( classInstance.teardown ) or + self.asFunction( classInstance.Teardown ) + if func then + self:updateStatus(self:protectedCall(classInstance, func, className..'.tearDown')) + end + end + end + + self:endTest() + end + + function M.LuaUnit.expandOneClass( result, className, classInstance ) + --[[ + Input: a list of { name, instance }, a class name, a class instance + Ouptut: modify result to add all test method instance in the form: + { className.methodName, classInstance } + ]] + for methodName, methodInstance in sortedPairs(classInstance) do + if M.LuaUnit.asFunction(methodInstance) and M.LuaUnit.isMethodTestName( methodName ) then + table.insert( result, { className..'.'..methodName, classInstance } ) + end + end + end + + function M.LuaUnit.expandClasses( listOfNameAndInst ) + --[[ + -- expand all classes (provided as {className, classInstance}) to a list of {className.methodName, classInstance} + -- functions and methods remain untouched + + Input: a list of { name, instance } + + Output: + * { function name, function instance } : do nothing + * { class.method name, class instance }: do nothing + * { class name, class instance } : add all method names in the form of (className.methodName, classInstance) + ]] + local result = {} + + for i,v in ipairs( listOfNameAndInst ) do + local name, instance = v[1], v[2] + if M.LuaUnit.asFunction(instance) then + table.insert( result, { name, instance } ) + else + if type(instance) ~= 'table' then + error( 'Instance must be a table or a function, not a '..type(instance)..' with value '..prettystr(instance)) + end + local className, methodName = M.LuaUnit.splitClassMethod( name ) + if className then + local methodInstance = instance[methodName] + if methodInstance == nil then + error( "Could not find method in class "..tostring(className).." for method "..tostring(methodName) ) + end + table.insert( result, { name, instance } ) + else + M.LuaUnit.expandOneClass( result, name, instance ) + end + end + end + + return result + end + + function M.LuaUnit.applyPatternFilter( patternIncFilter, listOfNameAndInst ) + local included, excluded = {}, {} + for i, v in ipairs( listOfNameAndInst ) do + -- local name, instance = v[1], v[2] + if patternFilter( patternIncFilter, v[1] ) then + table.insert( included, v ) + else + table.insert( excluded, v ) + end + end + return included, excluded + end + + function M.LuaUnit:runSuiteByInstances( listOfNameAndInst ) + --[[ Run an explicit list of tests. Each item of the list must be one of: + * { function name, function instance } + * { class name, class instance } + * { class.method name, class instance } + ]] + + local expandedList = self.expandClasses( listOfNameAndInst ) + if self.shuffle then + randomizeTable( expandedList ) + end + local filteredList, filteredOutList = self.applyPatternFilter( + self.patternIncludeFilter, expandedList ) + + self:startSuite( #filteredList, #filteredOutList ) + + for i,v in ipairs( filteredList ) do + local name, instance = v[1], v[2] + if M.LuaUnit.asFunction(instance) then + self:execOneFunction( nil, name, nil, instance ) + else + -- expandClasses() should have already taken care of sanitizing the input + assert( type(instance) == 'table' ) + local className, methodName = M.LuaUnit.splitClassMethod( name ) + assert( className ~= nil ) + local methodInstance = instance[methodName] + assert(methodInstance ~= nil) + self:execOneFunction( className, methodName, instance, methodInstance ) + end + if self.result.aborted then + break -- "--error" or "--failure" option triggered + end + end + + if self.lastClassName ~= nil then + self:endClass() + end + + self:endSuite() + + if self.result.aborted then + print("LuaUnit ABORTED (as requested by --error or --failure option)") + os.exit(-2) + end + end + + function M.LuaUnit:runSuiteByNames( listOfName ) + --[[ Run LuaUnit with a list of generic names, coming either from command-line or from global + namespace analysis. Convert the list into a list of (name, valid instances (table or function)) + and calls runSuiteByInstances. + ]] + + local instanceName, instance + local listOfNameAndInst = {} + + for i,name in ipairs( listOfName ) do + local className, methodName = M.LuaUnit.splitClassMethod( name ) + if className then + instanceName = className + instance = _G[instanceName] + + if instance == nil then + error( "No such name in global space: "..instanceName ) + end + + if type(instance) ~= 'table' then + error( 'Instance of '..instanceName..' must be a table, not '..type(instance)) + end + + local methodInstance = instance[methodName] + if methodInstance == nil then + error( "Could not find method in class "..tostring(className).." for method "..tostring(methodName) ) + end + + else + -- for functions and classes + instanceName = name + instance = _G[instanceName] + end + + if instance == nil then + error( "No such name in global space: "..instanceName ) + end + + if (type(instance) ~= 'table' and type(instance) ~= 'function') then + error( 'Name must match a function or a table: '..instanceName ) + end + + table.insert( listOfNameAndInst, { name, instance } ) + end + + self:runSuiteByInstances( listOfNameAndInst ) + end + + function M.LuaUnit.run(...) + -- Run some specific test classes. + -- If no arguments are passed, run the class names specified on the + -- command line. If no class name is specified on the command line + -- run all classes whose name starts with 'Test' + -- + -- If arguments are passed, they must be strings of the class names + -- that you want to run or generic command line arguments (-o, -p, -v, ...) + + local runner = M.LuaUnit.new() + return runner:runSuite(...) + end + + function M.LuaUnit:runSuite( ... ) + + local args = {...} + if type(args[1]) == 'table' and args[1].__class__ == 'LuaUnit' then + -- run was called with the syntax M.LuaUnit:runSuite() + -- we support both M.LuaUnit.run() and M.LuaUnit:run() + -- strip out the first argument + table.remove(args,1) + end + + if #args == 0 then + args = cmdline_argv + end + + local options = pcall_or_abort( M.LuaUnit.parseCmdLine, args ) + + -- We expect these option fields to be either `nil` or contain + -- valid values, so it's safe to always copy them directly. + self.verbosity = options.verbosity + self.quitOnError = options.quitOnError + self.quitOnFailure = options.quitOnFailure + + self.exeRepeat = options.exeRepeat + self.patternIncludeFilter = options.pattern + self.shuffle = options.shuffle + + if options.output then + if options.output:lower() == 'junit' and options.fname == nil then + print('With junit output, a filename must be supplied with -n or --name') + os.exit(-1) + end + pcall_or_abort(self.setOutputType, self, options.output, options.fname) + end + + self:runSuiteByNames( options.testNames or M.LuaUnit.collectTests() ) + + return self.result.notSuccessCount + end +-- class LuaUnit + +-- For compatbility with LuaUnit v2 +M.run = M.LuaUnit.run +M.Run = M.LuaUnit.run + +function M:setVerbosity( verbosity ) + M.LuaUnit.verbosity = verbosity +end +M.set_verbosity = M.setVerbosity +M.SetVerbosity = M.setVerbosity + + +return M diff --git a/scripts/fetch-linux.sh b/scripts/fetch-linux.sh new file mode 100755 index 0000000..9a1c382 --- /dev/null +++ b/scripts/fetch-linux.sh @@ -0,0 +1,8 @@ +#!/bin/sh +# script from https://blog.markvincze.com/download-artifacts-from-a-latest-github-release-in-sh-and-powershell/ +set -e +LATEST_RELEASE=$(curl -L -s -H 'Accept: application/json' https://github.com/launchdarkly/c-server-sdk/releases/latest) +LATEST_VERSION=$(echo $LATEST_RELEASE | sed -e 's/.*"tag_name":"\([^"]*\)".*/\1/') +ARTIFACT_URL="https://github.com/launchdarkly/c-server-sdk/releases/download/$LATEST_VERSION/linux-gcc-64bit-dynamic.zip" +curl -o linux-gcc-64bit-dynamic.zip -L $ARTIFACT_URL +unzip linux-gcc-64bit-dynamic.zip diff --git a/test.lua b/test.lua new file mode 100644 index 0000000..5d3bad2 --- /dev/null +++ b/test.lua @@ -0,0 +1,39 @@ +local u = require('luaunit') +local l = require("launchdarkly-server-sdk") + +function makeTestClient() + return l.clientInit({ + key = "sdk-test", + offline = true + }, 0) +end + +local user = l.makeUser({ key = "alice" }) + +function testBoolVariation() + local e = false + u.assertEquals(e, makeTestClient().boolVariation(user, "test", e)) +end + +function testIntVariation() + local e = 3 + u.assertEquals(e, makeTestClient().intVariation(user, "test", e)) +end + +function testDoubleVariation() + local e = 5.3 + u.assertEquals(e, makeTestClient().doubleVariation(user, "test", e)) +end + +function testStringVariation() + local e = "a" + u.assertEquals(e, makeTestClient().stringVariation(user, "test", e)) +end + +function testJSONVariation() + local e = { ["a"] = "b" } + u.assertEquals(e, makeTestClient().jsonVariation(user, "test", e)) +end + +local runner = u.LuaUnit.new() +os.exit(runner:runSuite()) From 89806d4a736f2440a5c34e4198cffd1d8fed1260 Mon Sep 17 00:00:00 2001 From: hroederld Date: Wed, 24 Jun 2020 15:23:03 -0700 Subject: [PATCH 016/161] [ch80958] remove beta warning --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index a5fe5b4..46a1e15 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ LaunchDarkly Server-Side SDK for Lua =========================== -*This version of the SDK is a **beta** version and should not be considered ready for production use while this message is visible.* - LaunchDarkly overview ------------------------- [LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves over 100 billion feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/docs/getting-started) using LaunchDarkly today! From 6b6a4c5a99463e37fe8b956a9eeb748278c4292e Mon Sep 17 00:00:00 2001 From: hroederld Date: Thu, 25 Jun 2020 16:07:46 -0700 Subject: [PATCH 017/161] fix timeout (#17) --- launchdarkly-server-sdk.lua | 7 +++++-- test.lua | 13 ++++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/launchdarkly-server-sdk.lua b/launchdarkly-server-sdk.lua index 06c4790..6a7a22e 100644 --- a/launchdarkly-server-sdk.lua +++ b/launchdarkly-server-sdk.lua @@ -446,10 +446,13 @@ end local function clientInit(config, timeoutMilliseconds) local interface = {} + if timeoutMilliseconds <= 0 then + timeoutMilliseconds = 1 + end + --- An opaque client object -- @type Client - - local client = ffi.gc(so.LDClientInit(makeConfig(config), 1000), so.LDClientClose) + local client = ffi.gc(so.LDClientInit(makeConfig(config), timeoutMilliseconds), so.LDClientClose) --- Check if a client has been fully initialized. This may be useful if the -- initialization timeout was reached. diff --git a/test.lua b/test.lua index 5d3bad2..5ec4e51 100644 --- a/test.lua +++ b/test.lua @@ -1,11 +1,18 @@ local u = require('luaunit') local l = require("launchdarkly-server-sdk") +function logger(level, line) + print(level .. ": " .. line) +end + +l.registerLogger("TRACE", logger) + function makeTestClient() - return l.clientInit({ - key = "sdk-test", - offline = true + local c = l.clientInit({ + key = "sdk-test" }, 0) + + return c end local user = l.makeUser({ key = "alice" }) From 39da875c5abe01d453749805d49eabce8e1a55de Mon Sep 17 00:00:00 2001 From: hroederld Date: Tue, 7 Jul 2020 14:01:11 -0700 Subject: [PATCH 018/161] [ch70870] redis module (#18) --- .circleci/config.yml | 16 ++++++-- .ldrelease/linux-prepare.sh | 3 +- launchdarkly-server-sdk-redis.lua | 63 +++++++++++++++++++++++++++++++ launchdarkly-server-sdk.lua | 8 +++- scripts/fetch-linux.sh | 8 ---- test.lua | 12 ++++++ 6 files changed, 96 insertions(+), 14 deletions(-) create mode 100644 launchdarkly-server-sdk-redis.lua delete mode 100755 scripts/fetch-linux.sh diff --git a/.circleci/config.yml b/.circleci/config.yml index da41be4..5bc7a42 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -10,6 +10,7 @@ jobs: build-test-linux: docker: - image: ubuntu:18.04 + - image: redis steps: - checkout - run: @@ -19,8 +20,17 @@ jobs: name: Build Doc command: ./.ldrelease/linux-build-docs.sh - run: - name: Fetch c-server-sdk - command: ./scripts/fetch-linux.sh + name: Build c-server-sdk + command: | + git clone https://github.com/launchdarkly/c-server-sdk.git + cd c-server-sdk + mkdir build + cd build + cmake -D REDIS_STORE=ON -D BUILD_SHARED_LIBS=ON -D BUILD_TESTING=OFF .. + make - run: name: Run tests - command: LD_LIBRARY_PATH=./lib luajit test.lua + command: | + cp c-server-sdk/build/libldserverapi.so . + cp c-server-sdk/build/stores/redis/libldserverapi-redis.so . + LD_LIBRARY_PATH=. luajit test.lua diff --git a/.ldrelease/linux-prepare.sh b/.ldrelease/linux-prepare.sh index b9e7621..1cdb702 100755 --- a/.ldrelease/linux-prepare.sh +++ b/.ldrelease/linux-prepare.sh @@ -3,4 +3,5 @@ set -e apt-get update -y && apt-get install -y luajit lua-ldoc zip ca-certificates \ - curl zip lua-cjson libpcre3 libcurl4-openssl-dev + curl zip lua-cjson libpcre3 libcurl4-openssl-dev cmake libhiredis-dev git \ + build-essential libpcre3-dev diff --git a/launchdarkly-server-sdk-redis.lua b/launchdarkly-server-sdk-redis.lua new file mode 100644 index 0000000..e1b13fb --- /dev/null +++ b/launchdarkly-server-sdk-redis.lua @@ -0,0 +1,63 @@ +--- Server-side SDK for LaunchDarkly Redis store. +-- @module launchdarkly-server-sdk-redis + +local ffi = require("ffi") + +ffi.cdef[[ + struct LDRedisConfig; + struct LDStoreInterface; + + struct LDRedisConfig *LDRedisConfigNew(); + + bool LDRedisConfigSetHost( + struct LDRedisConfig *const config, + const char *const host); + + bool LDRedisConfigSetPort( + struct LDRedisConfig *const config, + const unsigned short port); + + bool LDRedisConfigSetPrefix( + struct LDRedisConfig *const config, + const char *const prefix); + + bool LDRedisConfigSetPoolSize( + struct LDRedisConfig *const config, + const unsigned int poolSize); + + bool LDRedisConfigFree(struct LDRedisConfig *const config); + + struct LDStoreInterface *LDStoreInterfaceRedisNew( + struct LDRedisConfig *const config); +]] + +local so = ffi.load("ldserverapi-redis") + +local function applyWhenNotNil(subject, operation, value) + if value ~= nil and value ~= cjson.null then + operation(subject, value) + end +end + +--- Initialize a store backend +-- @tparam table fields list of configuration options +-- @tparam[opt] string fields.host Hostname for Redis. +-- @tparam[opt] int fields.port Port for Redis. +-- @tparam[opt] string fields.prefix Redis key prefix for SDK values. +-- @tparam[opt] int fields.poolSize Number of Redis connections to maintain. +-- @return A fresh Redis store backend. +local function makeStore(fields) + local config = so.LDRedisConfigNew() + + applyWhenNotNil(config, so.LDRedisConfigSetHost, fields["host"]) + applyWhenNotNil(config, so.LDRedisConfigSetPort, fields["port"]) + applyWhenNotNil(config, so.LDRedisConfigSetPrefix, fields["prefix"]) + applyWhenNotNil(config, so.LDRedisConfigSetPoolSize, fields["poolSize"]) + + return so.LDStoreInterfaceRedisNew(config) +end + +-- @export +return { + makeStore = makeStore +} diff --git a/launchdarkly-server-sdk.lua b/launchdarkly-server-sdk.lua index 6a7a22e..e2bb656 100644 --- a/launchdarkly-server-sdk.lua +++ b/launchdarkly-server-sdk.lua @@ -335,6 +335,7 @@ local function makeConfig(fields) applyWhenNotNil(config, so.LDConfigInlineUsersInEvents, fields["inlineUsersInEvents"]) applyWhenNotNil(config, so.LDConfigSetUserKeysCapacity, fields["userKeysCapacity"]) applyWhenNotNil(config, so.LDConfigSetUserKeysFlushInterval, fields["userKeysFlushInterval"]) + applyWhenNotNil(config, so.LDConfigSetFeatureStoreBackend, fields["featureStoreBackend"]) so.LDConfigSetWrapperInfo(config, "lua-server-sdk", SDKVersion) @@ -440,8 +441,11 @@ end -- @tparam[opt] table config.privateAttributeNames Marks a set of user attribute -- names private. Any users sent to LaunchDarkly with this configuration active -- will have attributes with these names removed. --- @tparam int timeoutMilliseconds How long to wait for flags to download. --- If the timeout is reached a non fully initialized client will be returned. +-- @tparam[opt] backend config.featureStoreBackend Persistent feature store +-- backend. +-- @tparam int timeoutMilliseconds How long to wait for flags to +-- download. If the timeout is reached a non fully initialized client will be +-- returned. -- @return A fresh client. local function clientInit(config, timeoutMilliseconds) local interface = {} diff --git a/scripts/fetch-linux.sh b/scripts/fetch-linux.sh deleted file mode 100755 index 9a1c382..0000000 --- a/scripts/fetch-linux.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh -# script from https://blog.markvincze.com/download-artifacts-from-a-latest-github-release-in-sh-and-powershell/ -set -e -LATEST_RELEASE=$(curl -L -s -H 'Accept: application/json' https://github.com/launchdarkly/c-server-sdk/releases/latest) -LATEST_VERSION=$(echo $LATEST_RELEASE | sed -e 's/.*"tag_name":"\([^"]*\)".*/\1/') -ARTIFACT_URL="https://github.com/launchdarkly/c-server-sdk/releases/download/$LATEST_VERSION/linux-gcc-64bit-dynamic.zip" -curl -o linux-gcc-64bit-dynamic.zip -L $ARTIFACT_URL -unzip linux-gcc-64bit-dynamic.zip diff --git a/test.lua b/test.lua index 5ec4e51..97a7257 100644 --- a/test.lua +++ b/test.lua @@ -1,5 +1,6 @@ local u = require('luaunit') local l = require("launchdarkly-server-sdk") +local r = require("launchdarkly-server-sdk-redis") function logger(level, line) print(level .. ": " .. line) @@ -42,5 +43,16 @@ function testJSONVariation() u.assertEquals(e, makeTestClient().jsonVariation(user, "test", e)) end +function testRedisBasic() + local c = l.clientInit({ + key = "sdk-test", + featureStoreBackend = r.makeStore({}) + }, 0) + + local e = false + + u.assertEquals(e, makeTestClient().boolVariation(user, "test", e)) +end + local runner = u.LuaUnit.new() os.exit(runner:runSuite()) From cbc133f7a7664209f9899b9bcc1c962da262195c Mon Sep 17 00:00:00 2001 From: hroederld Date: Wed, 8 Jul 2020 17:28:08 -0700 Subject: [PATCH 019/161] [ch70870] ldoc releaser redis (#19) --- .ldrelease/linux-build-docs.sh | 6 +++++- launchdarkly-server-sdk-redis.lua | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.ldrelease/linux-build-docs.sh b/.ldrelease/linux-build-docs.sh index 91557bb..d753a86 100755 --- a/.ldrelease/linux-build-docs.sh +++ b/.ldrelease/linux-build-docs.sh @@ -6,7 +6,11 @@ set -e PROJECT_DIR=$(pwd) -ldoc launchdarkly-server-sdk.lua +mkdir tmp +cp launchdarkly-server-sdk.lua tmp/ +cp launchdarkly-server-sdk-redis.lua tmp/ + +ldoc tmp mkdir -p $PROJECT_DIR/artifacts cd $PROJECT_DIR/doc diff --git a/launchdarkly-server-sdk-redis.lua b/launchdarkly-server-sdk-redis.lua index e1b13fb..3551f5f 100644 --- a/launchdarkly-server-sdk-redis.lua +++ b/launchdarkly-server-sdk-redis.lua @@ -57,7 +57,7 @@ local function makeStore(fields) return so.LDStoreInterfaceRedisNew(config) end --- @export +--- @export return { makeStore = makeStore } From ad96c4e1475c0fb4ac52f902d9ef0240685c2665 Mon Sep 17 00:00:00 2001 From: hroederld Date: Wed, 8 Jul 2020 21:15:51 -0700 Subject: [PATCH 020/161] [ch82399] document c server-side sdk version requirement (#20) --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 46a1e15..633d131 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,11 @@ Supported Lua versions This version of the LaunchDarkly SDK is compatible with the Lua 5.1 interpreter, and LuaJIT. Lua 5.3 is not supported due to FFI constraints. +Supported C server-side SDK versions +----------- + +This version of the Lua server-side SDK depends on the C server-side SDK. The minimum required version is `2.1.0`, and under `3.0.0`. + Getting started ----------- From 5eb0fdca1aa29169f3a7f68222d4a176c265167d Mon Sep 17 00:00:00 2001 From: hroederld Date: Fri, 10 Jul 2020 13:07:26 -0700 Subject: [PATCH 021/161] [ch82565] collect between tests (#21) --- test.lua | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/test.lua b/test.lua index 97a7257..d8c267d 100644 --- a/test.lua +++ b/test.lua @@ -8,9 +8,12 @@ end l.registerLogger("TRACE", logger) +TestAll = {} + function makeTestClient() local c = l.clientInit({ - key = "sdk-test" + key = "sdk-test", + offline = true }, 0) return c @@ -18,35 +21,40 @@ end local user = l.makeUser({ key = "alice" }) -function testBoolVariation() +function TestAll:tearDown() + collectgarbage("collect") +end + +function TestAll:testBoolVariation() local e = false u.assertEquals(e, makeTestClient().boolVariation(user, "test", e)) end -function testIntVariation() +function TestAll:testIntVariation() local e = 3 u.assertEquals(e, makeTestClient().intVariation(user, "test", e)) end -function testDoubleVariation() +function TestAll:testDoubleVariation() local e = 5.3 u.assertEquals(e, makeTestClient().doubleVariation(user, "test", e)) end -function testStringVariation() +function TestAll:testStringVariation() local e = "a" u.assertEquals(e, makeTestClient().stringVariation(user, "test", e)) end -function testJSONVariation() +function TestAll:testJSONVariation() local e = { ["a"] = "b" } u.assertEquals(e, makeTestClient().jsonVariation(user, "test", e)) end -function testRedisBasic() +function TestAll:testRedisBasic() local c = l.clientInit({ key = "sdk-test", - featureStoreBackend = r.makeStore({}) + featureStoreBackend = r.makeStore({}), + offline = true }, 0) local e = false From c5ab6f272eca64342b7e54543f386e6603a047ce Mon Sep 17 00:00:00 2001 From: hroederld Date: Mon, 20 Jul 2020 13:00:26 -0700 Subject: [PATCH 022/161] [ch82639] rewrite for lua 5.1 5.2 5.3 (#22) --- .circleci/config.yml | 89 +- .ldrelease/build-c-server.sh | 11 + .ldrelease/linux-build-docs.sh | 4 +- launchdarkly-server-sdk-1.0-0.rockspec | 29 + launchdarkly-server-sdk-redis-1.0-0.rockspec | 29 + launchdarkly-server-sdk-redis.c | 110 ++ launchdarkly-server-sdk-redis.lua | 63 - launchdarkly-server-sdk.c | 1218 ++++++++++++++++++ launchdarkly-server-sdk.lua | 671 ---------- test.lua | 75 +- 10 files changed, 1541 insertions(+), 758 deletions(-) create mode 100755 .ldrelease/build-c-server.sh create mode 100644 launchdarkly-server-sdk-1.0-0.rockspec create mode 100644 launchdarkly-server-sdk-redis-1.0-0.rockspec create mode 100644 launchdarkly-server-sdk-redis.c delete mode 100644 launchdarkly-server-sdk-redis.lua create mode 100644 launchdarkly-server-sdk.c delete mode 100644 launchdarkly-server-sdk.lua diff --git a/.circleci/config.yml b/.circleci/config.yml index 5bc7a42..8d7d012 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,10 +4,12 @@ workflows: version: 2 all: jobs: - - build-test-linux + - build-test-linux-luajit + - build-test-linux-lua53 + - build-test-linux-lua52 jobs: - build-test-linux: + build-test-linux-luajit: docker: - image: ubuntu:18.04 - image: redis @@ -15,22 +17,81 @@ jobs: - checkout - run: name: Prepare - command: ./.ldrelease/linux-prepare.sh + command: | + apt-get update -y + apt-get install -y luajit libluajit-5.1-dev lua-ldoc zip \ + ca-certificates curl zip lua-cjson libpcre3 libcurl4-openssl-dev \ + cmake libhiredis-dev git build-essential libpcre3-dev luarocks + + - run: + name: Build c-server-sdk + command: ./.ldrelease/build-c-server.sh + + - run: + name: Build lua-server-sdk + command: | + luarocks make launchdarkly-server-sdk-1.0-0.rockspec + luarocks make launchdarkly-server-sdk-redis-1.0-0.rockspec + + - run: + name: Run tests + command: | + luajit test.lua + + build-test-linux-lua53: + docker: + - image: ubuntu:18.04 + - image: redis + steps: + - checkout + - run: + name: Prepare + command: | + apt-get update -y + apt-get install -y lua5.3 liblua5.3-dev lua-ldoc zip \ + ca-certificates curl zip lua-cjson libpcre3 libcurl4-openssl-dev \ + cmake libhiredis-dev git build-essential libpcre3-dev luarocks + - run: - name: Build Doc - command: ./.ldrelease/linux-build-docs.sh + name: Build c-server-sdk + command: ./.ldrelease/build-c-server.sh + + - run: + name: Build lua-server-sdk + command: | + luarocks make launchdarkly-server-sdk-1.0-0.rockspec + luarocks make launchdarkly-server-sdk-redis-1.0-0.rockspec + + - run: + name: Run tests + command: | + lua5.3 test.lua + + build-test-linux-lua52: + docker: + - image: ubuntu:18.04 + - image: redis + steps: + - checkout + - run: + name: Prepare + command: | + apt-get update -y + apt-get install -y lua5.2 liblua5.2-dev lua-ldoc zip \ + ca-certificates curl zip lua-cjson libpcre3 libcurl4-openssl-dev \ + cmake libhiredis-dev git build-essential libpcre3-dev luarocks + - run: name: Build c-server-sdk + command: ./.ldrelease/build-c-server.sh + + - run: + name: Build lua-server-sdk command: | - git clone https://github.com/launchdarkly/c-server-sdk.git - cd c-server-sdk - mkdir build - cd build - cmake -D REDIS_STORE=ON -D BUILD_SHARED_LIBS=ON -D BUILD_TESTING=OFF .. - make + luarocks make launchdarkly-server-sdk-1.0-0.rockspec + luarocks make launchdarkly-server-sdk-redis-1.0-0.rockspec + - run: name: Run tests command: | - cp c-server-sdk/build/libldserverapi.so . - cp c-server-sdk/build/stores/redis/libldserverapi-redis.so . - LD_LIBRARY_PATH=. luajit test.lua + lua5.2 test.lua diff --git a/.ldrelease/build-c-server.sh b/.ldrelease/build-c-server.sh new file mode 100755 index 0000000..ffa9db3 --- /dev/null +++ b/.ldrelease/build-c-server.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +set -e + +git clone https://github.com/launchdarkly/c-server-sdk.git +cd c-server-sdk +mkdir build +cd build +cmake -D BUILD_SHARED_LIBS=ON -D BUILD_TESTING=OFF -D REDIS_STORE=ON .. +make +make install diff --git a/.ldrelease/linux-build-docs.sh b/.ldrelease/linux-build-docs.sh index d753a86..d2c3f2f 100755 --- a/.ldrelease/linux-build-docs.sh +++ b/.ldrelease/linux-build-docs.sh @@ -7,8 +7,8 @@ set -e PROJECT_DIR=$(pwd) mkdir tmp -cp launchdarkly-server-sdk.lua tmp/ -cp launchdarkly-server-sdk-redis.lua tmp/ +cp launchdarkly-server-sdk.c tmp/ +cp launchdarkly-server-sdk-redis.c tmp/ ldoc tmp diff --git a/launchdarkly-server-sdk-1.0-0.rockspec b/launchdarkly-server-sdk-1.0-0.rockspec new file mode 100644 index 0000000..e58ed20 --- /dev/null +++ b/launchdarkly-server-sdk-1.0-0.rockspec @@ -0,0 +1,29 @@ +package = "launchdarkly-server-sdk" + +version = "1.0-0" + +source = { + url = "." -- not online yet! +} + +dependencies = { + "lua >= 5.1, <= 5.4", +} + +external_dependencies = { + LD = { + header = "launchdarkly/api.h" + } +} + +build = { + type = "builtin", + modules = { + ["launchdarkly_server_sdk"] = { + sources = { "launchdarkly-server-sdk.c" }, + incdirs = {"$(LD_INCDIR)"}, + libdirs = {"$(LD_LIBDIR)"}, + libraries = {"ldserverapi"} + } + } +} diff --git a/launchdarkly-server-sdk-redis-1.0-0.rockspec b/launchdarkly-server-sdk-redis-1.0-0.rockspec new file mode 100644 index 0000000..1751204 --- /dev/null +++ b/launchdarkly-server-sdk-redis-1.0-0.rockspec @@ -0,0 +1,29 @@ +package = "launchdarkly-server-sdk-redis" + +version = "1.0-0" + +source = { + url = "." -- not online yet! +} + +dependencies = { + "lua >= 5.1, <= 5.4", +} + +external_dependencies = { + LDREDIS = { + header = "launchdarkly/store/redis.h" + } +} + +build = { + type = "builtin", + modules = { + ["launchdarkly_server_sdk_redis"] = { + sources = { "launchdarkly-server-sdk-redis.c" }, + incdirs = {"$(LDREDIS_INCDIR)"}, + libdirs = {"$(LDREDIS_LIBDIR)"}, + libraries = {"ldserverapi-redis"} + } + } +} diff --git a/launchdarkly-server-sdk-redis.c b/launchdarkly-server-sdk-redis.c new file mode 100644 index 0000000..606b63b --- /dev/null +++ b/launchdarkly-server-sdk-redis.c @@ -0,0 +1,110 @@ +/*** +Server-side SDK for LaunchDarkly Redis store. +@module launchdarkly-server-sdk-redis +*/ + +#include +#include +#include +#include +#include + +#include +#include + +/*** +Initialize a store backend +@function makeStore +@tparam table fields list of configuration options +@tparam[opt] string fields.host Hostname for Redis. +@tparam[opt] int fields.port Port for Redis. +@tparam[opt] string fields.prefix Redis key prefix for SDK values. +@tparam[opt] int fields.poolSize Number of Redis connections to maintain. +@return A fresh Redis store backend. +*/ +static int +LuaLDRedisMakeStore(lua_State *const l) +{ + struct LDRedisConfig *config; + struct LDStoreInterface *storeInterface; + + if (lua_gettop(l) != 1) { + return luaL_error(l, "expecting exactly 1 argument"); + } + + luaL_checktype(l, 1, LUA_TTABLE); + + config = LDRedisConfigNew(); + + lua_getfield(l, 1, "host"); + + if (lua_isstring(l, -1)) { + LDRedisConfigSetHost(config, luaL_checkstring(l, -1)); + } + + lua_getfield(l, 1, "prefix"); + + if (lua_isstring(l, -1)) { + LDRedisConfigSetPrefix(config, luaL_checkstring(l, -1)); + } + + lua_getfield(l, 1, "port"); + + if (lua_isnumber(l, -1)) { + LDRedisConfigSetPort(config, luaL_checkinteger(l, -1)); + } + + lua_getfield(l, 1, "poolSize"); + + if (lua_isnumber(l, -1)) { + LDRedisConfigSetPoolSize(config, luaL_checkinteger(l, -1)); + } + + storeInterface = LDStoreInterfaceRedisNew(config); + + struct LDStoreInterface **i = + (struct LDStoreInterface **)lua_newuserdata(l, sizeof(storeInterface)); + + *i = storeInterface; + + luaL_getmetatable(l, "LaunchDarklyStoreInterface"); + lua_setmetatable(l, -2); + + return 1; +} + +static const struct luaL_Reg launchdarkly_functions[] = { + { "makeStore", LuaLDRedisMakeStore }, + { NULL, NULL } +}; + +#if !defined LUA_VERSION_NUM || LUA_VERSION_NUM==501 +/* +** Adapted from Lua 5.2.0 +*/ +static void luaL_setfuncs (lua_State *L, const luaL_Reg *l, int nup) { + luaL_checkstack(L, nup+1, "too many upvalues"); + for (; l->name != NULL; l++) { /* fill the table with given functions */ + int i; + lua_pushstring(L, l->name); + for (i = 0; i < nup; i++) /* copy upvalues to the top */ + lua_pushvalue(L, -(nup+1)); + lua_pushcclosure(L, l->func, nup); /* closure with those upvalues */ + lua_settable(L, -(nup + 3)); + } + lua_pop(L, nup); /* remove upvalues */ +} +#endif + +int +luaopen_launchdarkly_server_sdk_redis(lua_State *const l) +{ + #if LUA_VERSION_NUM == 503 || LUA_VERSION_NUM == 502 + luaL_newlib(l, launchdarkly_functions); + #else + luaL_register(l, "launchdarkly-server-sdk-redis", + launchdarkly_functions); + #endif + + return 1; +} diff --git a/launchdarkly-server-sdk-redis.lua b/launchdarkly-server-sdk-redis.lua deleted file mode 100644 index 3551f5f..0000000 --- a/launchdarkly-server-sdk-redis.lua +++ /dev/null @@ -1,63 +0,0 @@ ---- Server-side SDK for LaunchDarkly Redis store. --- @module launchdarkly-server-sdk-redis - -local ffi = require("ffi") - -ffi.cdef[[ - struct LDRedisConfig; - struct LDStoreInterface; - - struct LDRedisConfig *LDRedisConfigNew(); - - bool LDRedisConfigSetHost( - struct LDRedisConfig *const config, - const char *const host); - - bool LDRedisConfigSetPort( - struct LDRedisConfig *const config, - const unsigned short port); - - bool LDRedisConfigSetPrefix( - struct LDRedisConfig *const config, - const char *const prefix); - - bool LDRedisConfigSetPoolSize( - struct LDRedisConfig *const config, - const unsigned int poolSize); - - bool LDRedisConfigFree(struct LDRedisConfig *const config); - - struct LDStoreInterface *LDStoreInterfaceRedisNew( - struct LDRedisConfig *const config); -]] - -local so = ffi.load("ldserverapi-redis") - -local function applyWhenNotNil(subject, operation, value) - if value ~= nil and value ~= cjson.null then - operation(subject, value) - end -end - ---- Initialize a store backend --- @tparam table fields list of configuration options --- @tparam[opt] string fields.host Hostname for Redis. --- @tparam[opt] int fields.port Port for Redis. --- @tparam[opt] string fields.prefix Redis key prefix for SDK values. --- @tparam[opt] int fields.poolSize Number of Redis connections to maintain. --- @return A fresh Redis store backend. -local function makeStore(fields) - local config = so.LDRedisConfigNew() - - applyWhenNotNil(config, so.LDRedisConfigSetHost, fields["host"]) - applyWhenNotNil(config, so.LDRedisConfigSetPort, fields["port"]) - applyWhenNotNil(config, so.LDRedisConfigSetPrefix, fields["prefix"]) - applyWhenNotNil(config, so.LDRedisConfigSetPoolSize, fields["poolSize"]) - - return so.LDStoreInterfaceRedisNew(config) -end - ---- @export -return { - makeStore = makeStore -} diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c new file mode 100644 index 0000000..a21186b --- /dev/null +++ b/launchdarkly-server-sdk.c @@ -0,0 +1,1218 @@ +/*** +Server-side SDK for LaunchDarkly. +@module launchdarkly-server-sdk +*/ + +#include +#include +#include +#include +#include + +#include + +#define SDKVersion "1.0.0-beta.2" + +static struct LDJSON * +LuaValueToJSON(lua_State *const l, const int i); + +static struct LDJSON * +LuaTableToJSON(lua_State *const l, const int i); + +static struct LDJSON * +LuaArrayToJSON(lua_State *const l, const int i); + +static void +LuaPushJSON(lua_State *const l, const struct LDJSON *const j); + +static int globalLoggingCallback; +static lua_State *globalLuaState; + +static void +logHandler(const LDLogLevel level, const char * const line) +{ + lua_rawgeti(globalLuaState, LUA_REGISTRYINDEX, globalLoggingCallback); + + lua_pushstring(globalLuaState, LDLogLevelToString(level)); + lua_pushstring(globalLuaState, line); + + lua_call(globalLuaState, 2, 0); +} + +static LDLogLevel +LuaStringToLogLevel(const char *const text) +{ + if (strcmp(text, "FATAL") == 0) { + return LD_LOG_FATAL; + } else if (strcmp(text, "CRITICAL") == 0) { + return LD_LOG_CRITICAL; + } else if (strcmp(text, "ERROR") == 0) { + return LD_LOG_ERROR; + } else if (strcmp(text, "WARNING") == 0) { + return LD_LOG_WARNING; + } else if (strcmp(text, "INFO") == 0) { + return LD_LOG_INFO; + } else if (strcmp(text, "DEBUG") == 0) { + return LD_LOG_DEBUG; + } else if (strcmp(text, "TRACE") == 0) { + return LD_LOG_TRACE; + } + + return LD_LOG_INFO; +} + +/*** +Set the global logger for all SDK operations. This function is not thread +safe, and if used should be done so before other operations. The default +log level is "INFO". +@function registerLogger +@tparam string logLevel The level to at. Available options are: +"FATAL", "CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "TRACE". +@tparam function cb The logging handler. Callback must be of the form +"function (logLevel, logLine) ... end". +*/ +static int +LuaLDRegisterLogger(lua_State *const l) +{ + if (lua_gettop(l) != 2) { + return luaL_error(l, "expecting exactly 2 arguments"); + } + + const char *const level = luaL_checkstring(l, 1); + + globalLuaState = l; + globalLoggingCallback = luaL_ref(l, LUA_REGISTRYINDEX); + + LDConfigureGlobalLogger(LuaStringToLogLevel(level), logHandler); + + return 0; +} + +static bool +isArray(lua_State *const l, const int i) +{ + lua_pushvalue(l, i); + + lua_pushnil(l); + + bool array = true; + + while (lua_next(l, -2) != 0) { + if (lua_type(l, -2) != LUA_TNUMBER) { + array = false; + } + + lua_pop(l, 1); + } + + lua_pop(l, 1); + + return array; +} + +static struct LDJSON * +LuaValueToJSON(lua_State *const l, const int i) +{ + struct LDJSON *result; + + switch (lua_type(l, i)) { + case LUA_TBOOLEAN: + result = LDNewBool(lua_toboolean(l, i)); + break; + case LUA_TNUMBER: + result = LDNewNumber(lua_tonumber(l, i)); + break; + case LUA_TSTRING: + result = LDNewText(lua_tostring(l, i)); + break; + case LUA_TTABLE: + if (isArray(l, i)) { + result = LuaArrayToJSON(l, i); + } else { + result = LuaTableToJSON(l, i); + } + break; + default: + result = LDNewNull(); + break; + } + + return result; +} + +static struct LDJSON * +LuaArrayToJSON(lua_State *const l, const int i) +{ + struct LDJSON *const result = LDNewArray(); + + lua_pushvalue(l, i); + + lua_pushnil(l); + + while (lua_next(l, -2) != 0) { + struct LDJSON *value = LuaValueToJSON(l, -1); + + LDArrayPush(result, value); + + lua_pop(l, 1); + } + + lua_pop(l, 1); + + return result; +} + +static struct LDJSON * +LuaTableToJSON(lua_State *const l, const int i) +{ + struct LDJSON *const result = LDNewObject(); + + lua_pushvalue(l, i); + + lua_pushnil(l); + + while (lua_next(l, -2) != 0) { + const char *const key = lua_tostring(l, -2); + struct LDJSON *const value = LuaValueToJSON(l, -1); + + LDObjectSetKey(result, key, value); + + lua_pop(l, 1); + } + + lua_pop(l, 1); + + return result; +} + +static void +LuaPushJSONObject(lua_State *const l, const struct LDJSON *const j) +{ + struct LDJSON *iter; + + lua_newtable(l); + + for (iter = LDGetIter(j); iter; iter = LDIterNext(iter)) { + LuaPushJSON(l, iter); + lua_setfield(l, -2, LDIterKey(iter)); + } +} + +static void +LuaPushJSONArray(lua_State *const l, const struct LDJSON *const j) +{ + struct LDJSON *iter; + + lua_newtable(l); + + int index = 1; + + for (iter = LDGetIter(j); iter; iter = LDIterNext(iter)) { + LuaPushJSON(l, iter); + lua_rawseti(l, -2, index); + index++; + } +} + +static void +LuaPushJSON(lua_State *const l, const struct LDJSON *const j) +{ + switch (LDJSONGetType(j)) { + case LDText: + lua_pushstring(l, LDGetText(j)); + break; + case LDBool: + lua_pushboolean(l, LDGetBool(j)); + break; + case LDNumber: + lua_pushnumber(l, LDGetNumber(j)); + break; + case LDObject: + LuaPushJSONObject(l, j); + break; + case LDArray: + LuaPushJSONArray(l, j); + break; + default: + lua_pushnil(l); + break; + } + + return; +} + +/*** +Create a new opaque user object. +@function makeUser +@tparam table fields list of user fields. +@tparam string fields.key The user's key +@tparam[opt] boolean fields.anonymous Mark the user as anonymous +@tparam[opt] string fields.ip Set the user's IP +@tparam[opt] string fields.firstName Set the user's first name +@tparam[opt] string fields.lastName Set the user's last name +@tparam[opt] string fields.email Set the user's email +@tparam[opt] string fields.name Set the user's name +@tparam[opt] string fields.avatar Set the user's avatar +@tparam[opt] string fields.country Set the user's country +@tparam[opt] string fields.secondary Set the user's secondary key +@tparam[opt] table fields.privateAttributeNames A list of attributes to +redact +@tparam[opt] table fields.custom Set the user's custom JSON +@return an opaque user object +*/ +static int +LuaLDUserNew(lua_State *const l) +{ + if (lua_gettop(l) != 1) { + return luaL_error(l, "expecting exactly 1 argument"); + } + + luaL_checktype(l, 1, LUA_TTABLE); + + lua_getfield(l, 1, "key"); + + const char *const key = luaL_checkstring(l, -1); + + struct LDUser *user = LDUserNew(key); + + lua_getfield(l, 1, "anonymous"); + + if (lua_isboolean(l, -1)) { + LDUserSetAnonymous(user, lua_toboolean(l, -1)); + } + + lua_getfield(l, 1, "ip"); + + if (lua_isstring(l, -1)) { + LDUserSetIP(user, luaL_checkstring(l,-1)); + }; + + lua_getfield(l, 1, "firstName"); + + if (lua_isstring(l, -1)) { + LDUserSetFirstName(user, luaL_checkstring(l,-1)); + } + + lua_getfield(l, 1, "lastName"); + + if (lua_isstring(l, -1)) { + LDUserSetLastName(user, luaL_checkstring(l, -1)); + } + + lua_getfield(l, 1, "email"); + + if (lua_isstring(l, -1)) { + LDUserSetEmail(user, luaL_checkstring(l, -1)); + } + + lua_getfield(l, 1, "name"); + + if (lua_isstring(l, -1)) { + LDUserSetName(user, luaL_checkstring(l, -1)); + } + + lua_getfield(l, 1, "avatar"); + + if (lua_isstring(l, -1)) { + LDUserSetAvatar(user, luaL_checkstring(l, -1)); + } + + lua_getfield(l, 1, "country"); + + if (lua_isstring(l, -1)) { + LDUserSetCountry(user, luaL_checkstring(l, -1)); + } + + lua_getfield(l, 1, "secondary"); + + if (lua_isstring(l, -1)) { + LDUserSetSecondary(user, luaL_checkstring(l, -1)); + } + + lua_getfield(l, 1, "custom"); + + if (lua_istable(l, -1)) { + LDUserSetCustom(user, LuaValueToJSON(l, -1)); + } + + lua_getfield(l, 1, "privateAttributeNames"); + + if (lua_istable(l, -1)) { + struct LDJSON *attrs, *iter; + attrs = LuaValueToJSON(l, -1); + + for (iter = LDGetIter(attrs); iter; iter = LDIterNext(iter)) { + LDUserAddPrivateAttribute(user, LDGetText(iter)); + } + + LDJSONFree(attrs); + } + + struct LDUser **u = (struct LDUser **)lua_newuserdata(l, sizeof(user)); + + *u = user; + + luaL_getmetatable(l, "LaunchDarklyUser"); + lua_setmetatable(l, -2); + + return 1; +} + +static int +LuaLDUserFree(lua_State *const l) +{ + struct LDUser **user; + + user = (struct LDUser **)luaL_checkudata(l, 1, "LaunchDarklyUser"); + + if (*user) { + LDUserFree(*user); + *user = NULL; + } + + return 0; +} + +static struct LDConfig * +makeConfig(lua_State *const l, const int i) +{ + struct LDConfig *config; + + luaL_checktype(l, i, LUA_TTABLE); + + lua_getfield(l, i, "key"); + + const char *const key = luaL_checkstring(l, -1); + + config = LDConfigNew(key); + + lua_getfield(l, i, "baseURI"); + + if (lua_isstring(l, -1)) { + LDConfigSetBaseURI(config, luaL_checkstring(l, -1)); + } + + lua_getfield(l, i, "streamURI"); + + if (lua_isstring(l, -1)) { + LDConfigSetStreamURI(config, luaL_checkstring(l, -1)); + } + + lua_getfield(l, i, "eventsURI"); + + if (lua_isstring(l, -1)) { + LDConfigSetEventsURI(config, luaL_checkstring(l, -1)); + } + + lua_getfield(l, i, "stream"); + + if (lua_isboolean(l, -1)) { + LDConfigSetStream(config, lua_toboolean(l, -1)); + } + + lua_getfield(l, i, "sendEvents"); + + if (lua_isstring(l, -1)) { + LDConfigSetSendEvents(config, lua_toboolean(l, -1)); + } + + lua_getfield(l, i, "eventsCapacity"); + + if (lua_isnumber(l, -1)) { + LDConfigSetEventsCapacity(config, luaL_checkinteger(l, -1)); + } + + lua_getfield(l, i, "timeout"); + + if (lua_isnumber(l, -1)) { + LDConfigSetTimeout(config, luaL_checkinteger(l, -1)); + } + + lua_getfield(l, i, "flushInterval"); + + if (lua_isnumber(l, -1)) { + LDConfigSetFlushInterval(config, luaL_checkinteger(l, -1)); + } + + lua_getfield(l, i, "pollInterval"); + + if (lua_isnumber(l, -1)) { + LDConfigSetPollInterval(config, luaL_checkinteger(l, -1)); + } + + lua_getfield(l, i, "offline"); + + if (lua_isboolean(l, -1)) { + LDConfigSetOffline(config, lua_toboolean(l, -1)); + } + + lua_getfield(l, i, "useLDD"); + + if (lua_isboolean(l, -1)) { + LDConfigSetUseLDD(config, lua_toboolean(l, -1)); + } + + lua_getfield(l, i, "inlineUsersInEvents"); + + if (lua_isboolean(l, -1)) { + LDConfigInlineUsersInEvents(config, lua_toboolean(l, -1)); + } + + lua_getfield(l, i, "allAttributesPrivate"); + + if (lua_isboolean(l, -1)) { + LDConfigSetAllAttributesPrivate(config, lua_toboolean(l, -1)); + } + + lua_getfield(l, i, "userKeysCapacity"); + + if (lua_isnumber(l, -1)) { + LDConfigSetUserKeysCapacity(config, luaL_checkinteger(l, -1)); + } + + lua_getfield(l, i, "featureStoreBackend"); + + if (lua_isuserdata(l, -1)) { + struct LDStoreInterface **storeInterface; + + storeInterface = (struct LDStoreInterface **) + luaL_checkudata(l, -1, "LaunchDarklyStoreInterface"); + + LDConfigSetFeatureStoreBackend(config, *storeInterface); + } + + lua_getfield(l, 1, "privateAttributeNames"); + + if (lua_istable(l, -1)) { + struct LDJSON *attrs, *iter; + attrs = LuaValueToJSON(l, -1); + + for (iter = LDGetIter(attrs); iter; iter = LDIterNext(iter)) { + LDConfigAddPrivateAttribute(config, LDGetText(iter)); + } + + LDJSONFree(attrs); + } + + LDConfigSetWrapperInfo(config, "lua-server-sdk", SDKVersion); + + return config; +} + +/*** +Initialize a new client, and connect to LaunchDarkly. +@function makeClient +@tparam table config list of configuration options +@tparam string config.key Environment SDK key +@tparam[opt] string config.baseURI Set the base URI for connecting to +LaunchDarkly. You probably don't need to set this unless instructed by +LaunchDarkly. +@tparam[opt] string config.streamURI Set the streaming URI for connecting to +LaunchDarkly. You probably don't need to set this unless instructed by +LaunchDarkly. +@tparam[opt] string config.eventsURI Set the events URI for connecting to +LaunchDarkly. You probably don't need to set this unless instructed by +LaunchDarkly. +@tparam[opt] boolean config.stream Enables or disables real-time streaming +flag updates. When set to false, an efficient caching polling mechanism is +used. We do not recommend disabling streaming unless you have been instructed +to do so by LaunchDarkly support. Defaults to true. +@tparam[opt] string config.sendEvents Sets whether to send analytics events +back to LaunchDarkly. By default, the client will send events. This differs +from Offline in that it only affects sending events, not streaming or +polling. +@tparam[opt] int config.eventsCapacity The capacity of the events buffer. +The client buffers up to this many events in memory before flushing. If the +capacity is exceeded before the buffer is flushed, events will be discarded. +@tparam[opt] int config.timeout The connection timeout to use when making +requests to LaunchDarkly. +@tparam[opt] int config.flushInterval he time between flushes of the event +buffer. Decreasing the flush interval means that the event buffer is less +likely to reach capacity. +@tparam[opt] int config.pollInterval The polling interval +(when streaming is disabled) in milliseconds. +@tparam[opt] boolean config.offline Sets whether this client is offline. +An offline client will not make any network connections to LaunchDarkly, +and will return default values for all feature flags. +@tparam[opt] boolean config.allAttributesPrivate Sets whether or not all user +attributes (other than the key) should be hidden from LaunchDarkly. If this +is true, all user attribute values will be private, not just the attributes +specified in PrivateAttributeNames. +@tparam[opt] boolean config.inlineUsersInEvents Set to true if you need to +see the full user details in every analytics event. +@tparam[opt] int config.userKeysCapacity The number of user keys that the +event processor can remember at an one time, so that duplicate user details +will not be sent in analytics. +@tparam[opt] int config.userKeysFlushInterval The interval at which the event +processor will reset its set of known user keys, in milliseconds. +@tparam[opt] table config.privateAttributeNames Marks a set of user attribute +names private. Any users sent to LaunchDarkly with this configuration active +will have attributes with these names removed. +@param[opt] backend config.featureStoreBackend Persistent feature store +backend. +@tparam int timeoutMilliseconds How long to wait for flags to +download. If the timeout is reached a non fully initialized client will be +returned. +@return A fresh client. +*/ +static int +LuaLDClientInit(lua_State *const l) +{ + struct LDClient *client; + struct LDConfig *config; + unsigned int timeout; + + if (lua_gettop(l) != 2) { + return luaL_error(l, "expecting exactly 2 arguments"); + } + + config = makeConfig(l, 1); + + timeout = luaL_checkinteger(l, 2); + + client = LDClientInit(config, timeout); + + struct LDClient **c = + (struct LDClient **)lua_newuserdata(l, sizeof(client)); + + *c = client; + + luaL_getmetatable(l, "LaunchDarklyClient"); + lua_setmetatable(l, -2); + + return 1; +} + +static int +LuaLDClientClose(lua_State *const l) +{ + struct LDClient **client; + + client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + + if (*client) { + LDClientClose(*client); + *client = NULL; + } + + return 0; +} + +static void +LuaPushDetails(lua_State *const l, struct LDDetails *const details, + struct LDJSON *const value) +{ + struct LDJSON *reason; + + reason = LDReasonToJSON(details); + + lua_newtable(l); + + LuaPushJSON(l, reason); + lua_setfield(l, -2, "reason"); + + if (details->hasVariation) { + lua_pushnumber(l, details->variationIndex); + lua_setfield(l, -2, "variationIndex"); + } + + LuaPushJSON(l, value); + lua_setfield(l, -2, "value"); + + LDDetailsClear(details); + LDJSONFree(value); + LDJSONFree(reason); +} + +/** +Evaluate a boolean flag +@class function +@name boolVariation +@tparam user user An opaque user object from @{makeUser} +@tparam string key The key of the flag to evaluate. +@tparam boolean fallback The value to return on error +@treturn boolean The evaluation result, or the fallback value +*/ +static int +LuaLDClientBoolVariation(lua_State *const l) +{ + struct LDClient **client; + struct LDUser **user; + + if (lua_gettop(l) != 4) { + return luaL_error(l, "expecting exactly 4 arguments"); + } + + client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + + user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); + + const char *const key = luaL_checkstring(l, 3); + + const int fallback = lua_toboolean(l, 4); + + const LDBoolean result = + LDBoolVariation(*client, *user, key, fallback, NULL); + + lua_pushboolean(l, result); + + return 1; +} + +/*** +Evaluate a boolean flag and return an explanation +@class function +@name boolVariationDetail +@tparam user user An opaque user object from @{makeUser} +@tparam string key The key of the flag to evaluate. +@tparam boolean fallback The value to return on error +@treturn table The evaluation explanation +*/ +static int +LuaLDClientBoolVariationDetail(lua_State *const l) +{ + struct LDClient **client; + struct LDUser **user; + struct LDDetails details; + + if (lua_gettop(l) != 4) { + return luaL_error(l, "expecting exactly 4 arguments"); + } + + LDDetailsInit(&details); + + client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + + user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); + + const char *const key = luaL_checkstring(l, 3); + + const int fallback = lua_toboolean(l, 4); + + const LDBoolean result = + LDBoolVariation(*client, *user, key, fallback, &details); + + LuaPushDetails(l, &details, LDNewBool(result)); + + return 1; +} + +/*** +Evaluate an integer flag +@class function +@name intVariation +@tparam user user An opaque user object from @{makeUser} +@tparam string key The key of the flag to evaluate. +@tparam int fallback The value to return on error +@treturn int The evaluation result, or the fallback value +*/ +static int +LuaLDClientIntVariation(lua_State *const l) +{ + struct LDClient **client; + struct LDUser **user; + + if (lua_gettop(l) != 4) { + return luaL_error(l, "expecting exactly 4 arguments"); + } + + client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + + user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); + + const char *const key = luaL_checkstring(l, 3); + + const int fallback = luaL_checkinteger(l, 4); + + const int result = LDIntVariation(*client, *user, key, fallback, NULL); + + lua_pushnumber(l, result); + + return 1; +} + +/*** +Evaluate an integer flag and return an explanation +@class function +@name intVariationDetail +@tparam user user An opaque user object from @{makeUser} +@tparam string key The key of the flag to evaluate. +@tparam int fallback The value to return on error +@treturn table The evaluation explanation +*/ +static int +LuaLDClientIntVariationDetail(lua_State *const l) +{ + struct LDClient **client; + struct LDUser **user; + struct LDDetails details; + + if (lua_gettop(l) != 4) { + return luaL_error(l, "expecting exactly 4 arguments"); + } + + LDDetailsInit(&details); + + client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + + user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); + + const char *const key = luaL_checkstring(l, 3); + + const int fallback = luaL_checkinteger(l, 4); + + const int result = LDIntVariation(*client, *user, key, fallback, &details); + + LuaPushDetails(l, &details, LDNewNumber(result)); + + return 1; +} + +/*** +Evaluate a double flag +@class function +@name doubleVariation +@tparam user user An opaque user object from @{makeUser} +@tparam string key The key of the flag to evaluate. +@tparam number fallback The value to return on error +@treturn double The evaluation result, or the fallback value +*/ +static int +LuaLDClientDoubleVariation(lua_State *const l) +{ + struct LDClient **client; + struct LDUser **user; + + if (lua_gettop(l) != 4) { + return luaL_error(l, "expecting exactly 4 arguments"); + } + + client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + + user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); + + const char *const key = luaL_checkstring(l, 3); + + const double fallback = lua_tonumber(l, 4); + + const double result = + LDDoubleVariation(*client, *user, key, fallback, NULL); + + lua_pushnumber(l, result); + + return 1; +} + +/*** +Evaluate a double flag and return an explanation +@class function +@name doubleVariationDetail +@tparam user user An opaque user object from @{makeUser} +@tparam string key The key of the flag to evaluate. +@tparam number fallback The value to return on error +@treturn table The evaluation explanation +*/ +static int +LuaLDClientDoubleVariationDetail(lua_State *const l) +{ + struct LDClient **client; + struct LDUser **user; + struct LDDetails details; + + if (lua_gettop(l) != 4) { + return luaL_error(l, "expecting exactly 4 arguments"); + } + + LDDetailsInit(&details); + + client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + + user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); + + const char *const key = luaL_checkstring(l, 3); + + const double fallback = lua_tonumber(l, 4); + + const double result = + LDDoubleVariation(*client, *user, key, fallback, &details); + + LuaPushDetails(l, &details, LDNewNumber(result)); + + return 1; +} + +/*** +Evaluate a string flag +@class function +@name stringVariation +@tparam user user An opaque user object from @{makeUser} +@tparam string key The key of the flag to evaluate. +@tparam string fallback The value to return on error +@treturn string The evaluation result, or the fallback value +*/ +static int +LuaLDClientStringVariation(lua_State *const l) +{ + struct LDClient **client; + struct LDUser **user; + + if (lua_gettop(l) != 4) { + return luaL_error(l, "expecting exactly 4 arguments"); + } + + client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + + user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); + + const char *const key = luaL_checkstring(l, 3); + + const char *const fallback = luaL_checkstring(l, 4); + + char *const result = + LDStringVariation(*client, *user, key, fallback, NULL); + + lua_pushstring(l, result); + + LDFree(result); + + return 1; +} + +/*** +Evaluate a string flag and return an explanation +@class function +@name stringVariationDetail +@tparam user user An opaque user object from @{makeUser} +@tparam string key The key of the flag to evaluate. +@tparam string fallback The value to return on error +@treturn table The evaluation explanation +*/ +static int +LuaLDClientStringVariationDetail(lua_State *const l) +{ + struct LDClient **client; + struct LDUser **user; + struct LDDetails details; + + if (lua_gettop(l) != 4) { + return luaL_error(l, "expecting exactly 4 arguments"); + } + + LDDetailsInit(&details); + + client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + + user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); + + const char *const key = luaL_checkstring(l, 3); + + const char *const fallback = luaL_checkstring(l, 4); + + char *const result = + LDStringVariation(*client, *user, key, fallback, &details); + + LuaPushDetails(l, &details, LDNewText(result)); + + LDFree(result); + + return 1; +} + +/*** +Evaluate a json flag +@class function +@name jsonVariation +@tparam user user An opaque user object from @{makeUser} +@tparam string key The key of the flag to evaluate. +@tparam table fallback The value to return on error +@treturn table The evaluation result, or the fallback value +*/ +static int +LuaLDClientJSONVariation(lua_State *const l) +{ + struct LDClient **client; + struct LDUser **user; + struct LDJSON *fallback, *result; + + if (lua_gettop(l) != 4) { + return luaL_error(l, "expecting exactly 4 arguments"); + } + + client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + + user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); + + const char *const key = luaL_checkstring(l, 3); + + fallback = LuaValueToJSON(l, 4); + + result = LDJSONVariation(*client, *user, key, fallback, NULL); + + LuaPushJSON(l, result); + + LDJSONFree(fallback); + LDJSONFree(result); + + return 1; +} + +/*** +Evaluate a json flag and return an explanation +@class function +@name jsonVariationDetail +@tparam user user An opaque user object from @{makeUser} +@tparam string key The key of the flag to evaluate. +@tparam table fallback The value to return on error +@treturn table The evaluation explanation +*/ +static int +LuaLDClientJSONVariationDetail(lua_State *const l) +{ + struct LDClient **client; + struct LDUser **user; + struct LDJSON *fallback, *result; + struct LDDetails details; + + if (lua_gettop(l) != 4) { + return luaL_error(l, "expecting exactly 4 arguments"); + } + + LDDetailsInit(&details); + + client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + + user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); + + const char *const key = luaL_checkstring(l, 3); + + fallback = LuaValueToJSON(l, 4); + + result = LDJSONVariation(*client, *user, key, fallback, &details); + + LuaPushDetails(l, &details, result); + + LDJSONFree(fallback); + + return 1; +} + +/*** +Immediately flushes queued events. +@function flush +@treturn nil +*/ +static int +LuaLDClientFlush(lua_State *const l) +{ + struct LDClient **client; + + if (lua_gettop(l) != 1) { + return luaL_error(l, "expecting exactly 1 argument"); + } + + client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + + LDClientFlush(*client); + + return 0; +} + +/*** +Reports that a user has performed an event. Custom data, and a metric +can be attached to the event as JSON. +@function track +@tparam string key The name of the event +@tparam user user An opaque user object from @{makeUser} +@tparam[opt] table data A value to be associated with an event +@tparam[optchain] number metric A value to be associated with an event +@treturn nil +*/ +static int +LuaLDClientTrack(lua_State *const l) +{ + struct LDClient **client; + struct LDUser **user; + struct LDJSON *value; + + if (lua_gettop(l) < 3 || lua_gettop(l) > 5) { + return luaL_error(l, "expecting 3-5 arguments"); + } + + client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + + const char *const key = luaL_checkstring(l, 2); + + user = (struct LDUser **)luaL_checkudata(l, 3, "LaunchDarklyUser"); + + if (lua_isnil(l, 4)) { + value = NULL; + } else { + value = LuaValueToJSON(l, 4); + } + + if (lua_gettop(l) == 5 && lua_isnumber(l, 5)) { + const double metric = luaL_checknumber(l, 5); + + LDClientTrackMetric(*client, key, *user, value, metric); + } else { + LDClientTrack(*client, key, *user, value); + } + + return 0; +} + +/*** +Check if a client has been fully initialized. This may be useful if the +initialization timeout was reached. +@function isInitialized +@treturn boolean true if fully initialized +*/ +static int +LuaLDClientIsInitialized(lua_State *const l) +{ + struct LDClient **client; + + if (lua_gettop(l) != 1) { + return luaL_error(l, "expecting exactly 1 argument"); + } + + client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + + lua_pushboolean(l, LDClientIsInitialized(*client)); + + return 1; +} + +/*** +Generates an identify event for a user. +@function identify +@tparam user user An opaque user object from @{makeUser} +@treturn nil +*/ +static int +LuaLDClientIdentify(lua_State *const l) +{ + struct LDClient **client; + struct LDUser **user; + + if (lua_gettop(l) != 2) { + return luaL_error(l, "expecting exactly 2 arguments"); + } + + client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); + + LDClientIdentify(*client, *user); + + return 0; +} + +/*** +Returns a map from feature flag keys to values for a given user. +This does not send analytics events back to LaunchDarkly. +@function allFlags +@tparam user user An opaque user object from @{makeUser} +@treturn table +*/ +static int +LuaLDClientAllFlags(lua_State *const l) +{ + struct LDClient **client; + struct LDUser **user; + struct LDJSON *result; + + if (lua_gettop(l) != 2) { + return luaL_error(l, "expecting exactly 2 arguments"); + } + + client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + + user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); + + result = LDAllFlags(*client, *user); + + LuaPushJSON(l, result); + + return 1; +} + +static const struct luaL_Reg launchdarkly_functions[] = { + { "clientInit", LuaLDClientInit }, + { "makeUser", LuaLDUserNew }, + { "registerLogger", LuaLDRegisterLogger }, + { NULL, NULL } +}; + +static const struct luaL_Reg launchdarkly_client_methods[] = { + { "boolVariation", LuaLDClientBoolVariation }, + { "boolVariationDetail", LuaLDClientBoolVariationDetail }, + { "intVariation", LuaLDClientIntVariation }, + { "intVariationDetail", LuaLDClientIntVariationDetail }, + { "doubleVariation", LuaLDClientDoubleVariation }, + { "doubleVariationDetail", LuaLDClientDoubleVariationDetail }, + { "stringVariation", LuaLDClientStringVariation }, + { "stringVariationDetail", LuaLDClientStringVariationDetail }, + { "jsonVariation", LuaLDClientJSONVariation }, + { "jsonVariationDetail", LuaLDClientJSONVariationDetail }, + { "flush", LuaLDClientFlush }, + { "track", LuaLDClientTrack }, + { "allFlags", LuaLDClientAllFlags }, + { "isInitialized", LuaLDClientIsInitialized }, + { "identify", LuaLDClientIdentify }, + { "__gc", LuaLDClientClose }, + { NULL, NULL } +}; + +static const struct luaL_Reg launchdarkly_user_methods[] = { + { "__gc", LuaLDUserFree }, + { NULL, NULL } +}; + +static const struct luaL_Reg launchdarkly_store_methods[] = { + { NULL, NULL } +}; + +#if !defined LUA_VERSION_NUM || LUA_VERSION_NUM==501 +/* +** Adapted from Lua 5.2.0 +*/ +static void luaL_setfuncs (lua_State *L, const luaL_Reg *l, int nup) { + luaL_checkstack(L, nup+1, "too many upvalues"); + for (; l->name != NULL; l++) { /* fill the table with given functions */ + int i; + lua_pushstring(L, l->name); + for (i = 0; i < nup; i++) /* copy upvalues to the top */ + lua_pushvalue(L, -(nup+1)); + lua_pushcclosure(L, l->func, nup); /* closure with those upvalues */ + lua_settable(L, -(nup + 3)); + } + lua_pop(L, nup); /* remove upvalues */ +} +#endif + +int +luaopen_launchdarkly_server_sdk(lua_State *const l) +{ + luaL_newmetatable(l, "LaunchDarklyClient"); + lua_pushvalue(l, -1); + lua_setfield(l, -2, "__index"); + luaL_setfuncs(l, launchdarkly_client_methods, 0); + + luaL_newmetatable(l, "LaunchDarklyUser"); + lua_pushvalue(l, -1); + lua_setfield(l, -2, "__index"); + luaL_setfuncs(l, launchdarkly_user_methods, 0); + + luaL_newmetatable(l, "LaunchDarklyStoreInterface"); + lua_pushvalue(l, -1); + lua_setfield(l, -2, "__index"); + luaL_setfuncs(l, launchdarkly_store_methods, 0); + + #if LUA_VERSION_NUM == 503 || LUA_VERSION_NUM == 502 + luaL_newlib(l, launchdarkly_functions); + #else + luaL_register(l, "launchdarkly-server-sdk", launchdarkly_functions); + #endif + + return 1; +} diff --git a/launchdarkly-server-sdk.lua b/launchdarkly-server-sdk.lua deleted file mode 100644 index e2bb656..0000000 --- a/launchdarkly-server-sdk.lua +++ /dev/null @@ -1,671 +0,0 @@ ---- Server-side SDK for LaunchDarkly. --- @module launchdarkly-server-sdk - -local ffi = require("ffi") -local cjson = require("cjson") - -ffi.cdef[[ - struct LDJSON; - typedef enum { - LDNull = 0, - LDText, - LDNumber, - LDBool, - LDObject, - LDArray - } LDJSONType; - struct LDJSON * LDNewNull(); - struct LDJSON * LDNewBool(const bool boolean); - struct LDJSON * LDNewNumber(const double number); - struct LDJSON * LDNewText(const char *const text); - struct LDJSON * LDNewObject(); - struct LDJSON * LDNewArray(); - bool LDSetNumber(struct LDJSON *const node, const double number); - void LDJSONFree(struct LDJSON *const json); - struct LDJSON * LDJSONDuplicate(const struct LDJSON *const json); - LDJSONType LDJSONGetType(const struct LDJSON *const json); - bool LDJSONCompare(const struct LDJSON *const left, - const struct LDJSON *const right); - bool LDGetBool(const struct LDJSON *const node); - double LDGetNumber(const struct LDJSON *const node); - const char * LDGetText(const struct LDJSON *const node); - struct LDJSON * LDIterNext(const struct LDJSON *const iter); - struct LDJSON * LDGetIter(const struct LDJSON *const collection); - const char * LDIterKey(const struct LDJSON *const iter); - unsigned int LDCollectionGetSize( - const struct LDJSON *const collection); - struct LDJSON * LDCollectionDetachIter( - struct LDJSON *const collection, struct LDJSON *const iter); - struct LDJSON * LDArrayLookup(const struct LDJSON *const array, - const unsigned int index); - bool LDArrayPush(struct LDJSON *const array, - struct LDJSON *const item); - bool LDArrayAppend(struct LDJSON *const prefix, - const struct LDJSON *const suffix); - struct LDJSON * LDObjectLookup(const struct LDJSON *const object, - const char *const key); - bool LDObjectSetKey(struct LDJSON *const object, - const char *const key, struct LDJSON *const item); - void LDObjectDeleteKey(struct LDJSON *const object, - const char *const key); - struct LDJSON * LDObjectDetachKey(struct LDJSON *const object, - const char *const key); - bool LDObjectMerge(struct LDJSON *const to, - const struct LDJSON *const from); - char * LDJSONSerialize(const struct LDJSON *const json); - struct LDJSON * LDJSONDeserialize(const char *const text); - struct LDStoreInterface; struct LDConfig; - struct LDConfig * LDConfigNew(const char *const key); - void LDConfigFree(struct LDConfig *const config); - bool LDConfigSetBaseURI(struct LDConfig *const config, - const char *const baseURI); - bool LDConfigSetStreamURI(struct LDConfig *const config, - const char *const streamURI); - bool LDConfigSetEventsURI(struct LDConfig *const config, - const char *const eventsURI); - void LDConfigSetStream(struct LDConfig *const config, - const bool stream); - void LDConfigSetSendEvents(struct LDConfig *const config, - const bool sendEvents); - void LDConfigSetEventsCapacity(struct LDConfig *const config, - const unsigned int eventsCapacity); - void LDConfigSetTimeout(struct LDConfig *const config, - const unsigned int milliseconds); - void LDConfigSetFlushInterval(struct LDConfig *const config, - const unsigned int milliseconds); - void LDConfigSetPollInterval(struct LDConfig *const config, - const unsigned int milliseconds); - void LDConfigSetOffline(struct LDConfig *const config, - const bool offline); - void LDConfigSetUseLDD(struct LDConfig *const config, - const bool useLDD); - void LDConfigSetAllAttributesPrivate(struct LDConfig *const config, - const bool allAttributesPrivate); - void LDConfigInlineUsersInEvents(struct LDConfig *const config, - const bool inlineUsersInEvents); - void LDConfigSetUserKeysCapacity(struct LDConfig *const config, - const unsigned int userKeysCapacity); - void LDConfigSetUserKeysFlushInterval(struct LDConfig *const config, - const unsigned int milliseconds); - bool LDConfigAddPrivateAttribute(struct LDConfig *const config, - const char *const attribute); - void LDConfigSetFeatureStoreBackend(struct LDConfig *const config, - struct LDStoreInterface *const backend); - bool LDConfigSetWrapperInfo(struct LDConfig *const config, - const char *const wrapperName, const char *const wrapperVersion); - struct LDUser * LDUserNew(const char *const userkey); - void LDUserFree(struct LDUser *const user); - void LDUserSetAnonymous(struct LDUser *const user, const bool anon); - bool LDUserSetIP(struct LDUser *const user, const char *const ip); - bool LDUserSetFirstName(struct LDUser *const user, - const char *const firstName); - bool LDUserSetLastName(struct LDUser *const user, - const char *const lastName); - bool LDUserSetEmail(struct LDUser *const user, - const char *const email); - bool LDUserSetName(struct LDUser *const user, - const char *const name); - bool LDUserSetAvatar(struct LDUser *const user, - const char *const avatar); - bool LDUserSetCountry(struct LDUser *const user, - const char *const country); - bool LDUserSetSecondary(struct LDUser *const user, - const char *const secondary); - void LDUserSetCustom(struct LDUser *const user, - struct LDJSON *const custom); - bool LDUserAddPrivateAttribute(struct LDUser *const user, - const char *const attribute); - struct LDClient * LDClientInit(struct LDConfig *const config, - const unsigned int maxwaitmilli); - void LDClientClose(struct LDClient *const client); - bool LDClientIsInitialized(struct LDClient *const client); - bool LDClientTrack(struct LDClient *const client, - const char *const key, const struct LDUser *const user, - struct LDJSON *const data); - bool LDClientTrackMetric(struct LDClient *const client, - const char *const key, const struct LDUser *const user, - struct LDJSON *const data, const double metric); - bool LDClientIdentify(struct LDClient *const client, - const struct LDUser *const user); - bool LDClientIsOffline(struct LDClient *const client); - void LDClientFlush(struct LDClient *const client); - void * LDAlloc(const size_t bytes); - void LDFree(void *const buffer); - char * LDStrDup(const char *const string); - void * LDRealloc(void *const buffer, const size_t bytes); - void * LDCalloc(const size_t nmemb, const size_t size); - char * LDStrNDup(const char *const str, const size_t n); - void LDSetMemoryRoutines(void *(*const newMalloc)(const size_t), - void (*const newFree)(void *const), - void *(*const newRealloc)(void *const, const size_t), - char *(*const newStrDup)(const char *const), - void *(*const newCalloc)(const size_t, const size_t), - char *(*const newStrNDup)(const char *const, const size_t)); - void LDGlobalInit(); - typedef enum { - LD_LOG_FATAL = 0, - LD_LOG_CRITICAL, - LD_LOG_ERROR, - LD_LOG_WARNING, - LD_LOG_INFO, - LD_LOG_DEBUG, - LD_LOG_TRACE - } LDLogLevel; - void LDi_log(const LDLogLevel level, const char *const format, ...); - void LDBasicLogger(const LDLogLevel level, const char *const text); - void LDConfigureGlobalLogger(const LDLogLevel level, - void (*logger)(const LDLogLevel level, const char *const text)); - const char * LDLogLevelToString(const LDLogLevel level); - enum LDEvalReason { - LD_UNKNOWN = 0, - LD_ERROR, - LD_OFF, - LD_PREREQUISITE_FAILED, - LD_TARGET_MATCH, - LD_RULE_MATCH, - LD_FALLTHROUGH - }; - enum LDEvalErrorKind { - LD_CLIENT_NOT_READY, - LD_NULL_KEY, - LD_STORE_ERROR, - LD_FLAG_NOT_FOUND, - LD_USER_NOT_SPECIFIED, - LD_MALFORMED_FLAG, - LD_WRONG_TYPE, - LD_OOM - }; - struct LDDetailsRule { - unsigned int ruleIndex; - char *id; - }; - struct LDDetails { - unsigned int variationIndex; - bool hasVariation; - enum LDEvalReason reason; - union { - enum LDEvalErrorKind errorKind; - char *prerequisiteKey; - struct LDDetailsRule rule; - } extra; - }; - void LDDetailsInit(struct LDDetails *const details); - void LDDetailsClear(struct LDDetails *const details); - const char * LDEvalReasonKindToString(const enum LDEvalReason kind); - const char * LDEvalErrorKindToString( - const enum LDEvalErrorKind kind); - struct LDJSON * LDReasonToJSON( - const struct LDDetails *const details); - bool LDBoolVariation(struct LDClient *const client, - struct LDUser *const user, const char *const key, const bool fallback, - struct LDDetails *const details); - int LDIntVariation(struct LDClient *const client, - struct LDUser *const user, const char *const key, const int fallback, - struct LDDetails *const details); - double LDDoubleVariation(struct LDClient *const client, - struct LDUser *const user, const char *const key, const double fallback, - struct LDDetails *const details); - char * LDStringVariation(struct LDClient *const client, - struct LDUser *const user, const char *const key, - const char* const fallback, struct LDDetails *const details); - struct LDJSON * LDJSONVariation(struct LDClient *const client, - struct LDUser *const user, const char *const key, - const struct LDJSON *const fallback, struct LDDetails *const details); - struct LDJSON * LDAllFlags(struct LDClient *const client, - struct LDUser *const user); -]] - -local SDKVersion = "1.0.0-beta.2" - -local so = ffi.load("ldserverapi") - -local function applyWhenNotNil(subject, operation, value) - if value ~= nil and value ~= cjson.null then - operation(subject, value) - end -end - -local function toLaunchDarklyJSON(x) - return ffi.gc(so.LDJSONDeserialize(cjson.encode(x)), so.LDJSONFree) -end - -local function toLaunchDarklyJSONTransfer(x) - return so.LDJSONDeserialize(cjson.encode(x)) -end - -local function fromLaunchDarklyJSON(x) - local raw = so.LDJSONSerialize(x) - local native = ffi.string(raw) - so.LDFree(raw) - return cjson.decode(native) -end - ---- Details associated with an evaluation --- @name Details --- @class table --- @tfield[opt] int variationIndex The index of the returned value within the --- flag's list of variations. --- @field value The resulting value of an evaluation --- @tfield table reason The reason a specific value was returned --- @tfield string reason.kind The kind of reason --- @tfield[opt] string reason.errorKind If the kind is LD_ERROR, this contains --- the error string. --- @tfield[opt] string reason.ruleId If the kind is LD_RULE_MATCH this contains --- the id of the rule. --- @tfield[opt] int reason.ruleIndex If the kind is LD_RULE_MATCH this contains --- the index of the rule. --- @tfield[opt] string reason.prerequisiteKey If the kind is --- LD_PREREQUISITE_FAILED this contains the key of the failed prerequisite. - -local function convertDetails(cDetails, value) - local details = {} - local cReasonJSON = so.LDReasonToJSON(cDetails) - details.reason = fromLaunchDarklyJSON(cReasonJSON) - - if cDetails.hasVariation then - details.variationIndex = cDetails.variationIndex - end - - details.value = value - return details -end - -local function genericVariationDetail(client, user, key, fallback, variation, valueConverter) - local cDetails = ffi.new("struct LDDetails") - so.LDDetailsInit(cDetails) - local value = variation(client, user, key, fallback, cDetails) - if valueConverter ~= nil then - value = valueConverter(value) - end - local details = convertDetails(cDetails, value) - so.LDDetailsClear(cDetails) - return details -end - -local function stringToLogLevel(level) - local translation = { - ["FATAL"] = so.LD_LOG_FATAL, - ["CRITICAL"] = so.LD_LOG_CRITICAL, - ["ERROR"] = so.LD_LOG_ERROR, - ["WARNING"] = so.LD_LOG_WARNING, - ["INFO"] = so.LD_LOG_INFO, - ["DEBUG"] = so.LD_LOG_DEBUG, - ["TRACE"] = so.LD_LOG_TRACE - } - - local lookup = translation[level] - - if lookup == nil then - return so.LD_LOG_INFO - else - return lookup - end -end - ---- Set the global logger for all SDK operations. This function is not thread --- safe, and if used should be done so before other operations. The default --- log level is "INFO". --- @tparam string logLevel The level to at. Available options are: --- "FATAL", "CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "TRACE". --- @tparam function cb The logging handler. Callback must be of the form --- "function (logLevel, logLine) ... end". -local function registerLogger(logLevel, cb) - so.LDConfigureGlobalLogger(stringToLogLevel(logLevel), - function(logLevel, line) - cb(ffi.string(so.LDLogLevelToString(logLevel)), ffi.string(line)) - end - ) -end - ---- make a config -local function makeConfig(fields) - local config = so.LDConfigNew(fields["key"]) - - applyWhenNotNil(config, so.LDConfigSetBaseURI, fields["baseURI"]) - applyWhenNotNil(config, so.LDConfigSetStreamURI, fields["streamURI"]) - applyWhenNotNil(config, so.LDConfigSetEventsURI, fields["eventsURI"]) - applyWhenNotNil(config, so.LDConfigSetStream, fields["stream"]) - applyWhenNotNil(config, so.LDConfigSetSendEvents, fields["sendEvents"]) - applyWhenNotNil(config, so.LDConfigSetEventsCapacity, fields["eventsCapacity"]) - applyWhenNotNil(config, so.LDConfigSetTimeout, fields["timeout"]) - applyWhenNotNil(config, so.LDConfigSetFlushInterval, fields["flushInterval"]) - applyWhenNotNil(config, so.LDConfigSetPollInterval, fields["pollInterval"]) - applyWhenNotNil(config, so.LDConfigSetOffline, fields["offline"]) - applyWhenNotNil(config, so.LDConfigSetAllAttributesPrivate, fields["allAttributesPrivate"]) - applyWhenNotNil(config, so.LDConfigInlineUsersInEvents, fields["inlineUsersInEvents"]) - applyWhenNotNil(config, so.LDConfigSetUserKeysCapacity, fields["userKeysCapacity"]) - applyWhenNotNil(config, so.LDConfigSetUserKeysFlushInterval, fields["userKeysFlushInterval"]) - applyWhenNotNil(config, so.LDConfigSetFeatureStoreBackend, fields["featureStoreBackend"]) - - so.LDConfigSetWrapperInfo(config, "lua-server-sdk", SDKVersion) - - local names = fields["privateAttributeNames"] - - if names ~= nil and names ~= cjson.null then - for _, v in ipairs(names) do - so.LDConfigAddPrivateAttribute(config, v) - end - end - - return config -end - ---- Create a new opaque user object. --- @tparam table fields list of user fields. --- @tparam string fields.key The user's key --- @tparam[opt] boolean fields.anonymous Mark the user as anonymous --- @tparam[opt] string fields.ip Set the user's IP --- @tparam[opt] string fields.firstName Set the user's first name --- @tparam[opt] string fields.lastName Set the user's last name --- @tparam[opt] string fields.email Set the user's email --- @tparam[opt] string fields.name Set the user's name --- @tparam[opt] string fields.avatar Set the user's avatar --- @tparam[opt] string fields.country Set the user's country --- @tparam[opt] string fields.secondary Set the user's secondary key --- @tparam[opt] table fields.privateAttributeNames A list of attributes to --- redact --- @tparam[opt] table fields.custom Set the user's custom JSON --- @return an opaque user object -local function makeUser(fields) - local user = ffi.gc(so.LDUserNew(fields["key"]), so.LDUserFree) - - applyWhenNotNil(user, so.LDUserSetAnonymous, fields["anonymous"]) - applyWhenNotNil(user, so.LDUserSetIP, fields["ip"]) - applyWhenNotNil(user, so.LDUserSetFirstName, fields["firstName"]) - applyWhenNotNil(user, so.LDUserSetLastName, fields["lastName"]) - applyWhenNotNil(user, so.LDUserSetEmail, fields["email"]) - applyWhenNotNil(user, so.LDUserSetName, fields["name"]) - applyWhenNotNil(user, so.LDUserSetAvatar, fields["avatar"]) - applyWhenNotNil(user, so.LDUserSetCountry, fields["country"]) - applyWhenNotNil(user, so.LDUserSetSecondary, fields["secondary"]) - - if fields["custom"] ~= nil then - so.LDUserSetCustom(user, toLaunchDarklyJSONTransfer(fields["custom"])) - end - - local names = fields["privateAttributeNames"] - - if names ~= nil and names ~= cjson.null then - for _, v in ipairs(names) do - so.LDUserAddPrivateAttribute(user, v) - end - end - - return user -end - ---- Initialize a new client, and connect to LaunchDarkly. --- @tparam table config list of configuration options --- @tparam string config.key Environment SDK key --- @tparam[opt] string config.baseURI Set the base URI for connecting to --- LaunchDarkly. You probably don't need to set this unless instructed by --- LaunchDarkly. --- @tparam[opt] string config.streamURI Set the streaming URI for connecting to --- LaunchDarkly. You probably don't need to set this unless instructed by --- LaunchDarkly. --- @tparam[opt] string config.eventsURI Set the events URI for connecting to --- LaunchDarkly. You probably don't need to set this unless instructed by --- LaunchDarkly. --- @tparam[opt] boolean config.stream Enables or disables real-time streaming --- flag updates. When set to false, an efficient caching polling mechanism is --- used. We do not recommend disabling streaming unless you have been instructed --- to do so by LaunchDarkly support. Defaults to true. --- @tparam[opt] string config.sendEvents Sets whether to send analytics events --- back to LaunchDarkly. By default, the client will send events. This differs --- from Offline in that it only affects sending events, not streaming or --- polling. --- @tparam[opt] int config.eventsCapacity The capacity of the events buffer. --- The client buffers up to this many events in memory before flushing. If the --- capacity is exceeded before the buffer is flushed, events will be discarded. --- @tparam[opt] int config.timeout The connection timeout to use when making --- requests to LaunchDarkly. --- @tparam[opt] int config.flushInterval he time between flushes of the event --- buffer. Decreasing the flush interval means that the event buffer is less --- likely to reach capacity. --- @tparam[opt] int config.pollInterval The polling interval --- (when streaming is disabled) in milliseconds. --- @tparam[opt] boolean config.offline Sets whether this client is offline. --- An offline client will not make any network connections to LaunchDarkly, --- and will return default values for all feature flags. --- @tparam[opt] boolean config.allAttributesPrivate Sets whether or not all user --- attributes (other than the key) should be hidden from LaunchDarkly. If this --- is true, all user attribute values will be private, not just the attributes --- specified in PrivateAttributeNames. --- @tparam[opt] boolean config.inlineUsersInEvents Set to true if you need to --- see the full user details in every analytics event. --- @tparam[opt] int config.userKeysCapacity The number of user keys that the --- event processor can remember at an one time, so that duplicate user details --- will not be sent in analytics. --- @tparam[opt] int config.userKeysFlushInterval The interval at which the event --- processor will reset its set of known user keys, in milliseconds. --- @tparam[opt] table config.privateAttributeNames Marks a set of user attribute --- names private. Any users sent to LaunchDarkly with this configuration active --- will have attributes with these names removed. --- @tparam[opt] backend config.featureStoreBackend Persistent feature store --- backend. --- @tparam int timeoutMilliseconds How long to wait for flags to --- download. If the timeout is reached a non fully initialized client will be --- returned. --- @return A fresh client. -local function clientInit(config, timeoutMilliseconds) - local interface = {} - - if timeoutMilliseconds <= 0 then - timeoutMilliseconds = 1 - end - - --- An opaque client object - -- @type Client - local client = ffi.gc(so.LDClientInit(makeConfig(config), timeoutMilliseconds), so.LDClientClose) - - --- Check if a client has been fully initialized. This may be useful if the - -- initialization timeout was reached. - -- @class function - -- @name isInitialized - -- @treturn boolean true if fully initialized - interface.isInitialized = function() - return so.LDClientIsInitialized(client) - end - - --- Generates an identify event for a user. - -- @class function - -- @name identify - -- @tparam user user An opaque user object from @{makeUser} - -- @treturn nil - interface.identify = function(user) - so.LDClientIdentify(client, user) - end - - --- Whether the LaunchDarkly client is in offline mode. - -- @class function - -- @name isOffline - -- @treturn boolean true if offline - interface.isOffline = function() - so.LDClientIsOffline(client) - end - - --- Immediately flushes queued events. - -- @class function - -- @name flush - -- @treturn nil - interface.flush = function() - so.LDClientFlush(client) - end - - --- Reports that a user has performed an event. Custom data, and a metric - -- can be attached to the event as JSON. - -- @class function - -- @name track - -- @tparam string key The name of the event - -- @tparam user user An opaque user object from @{makeUser} - -- @tparam[opt] table data A value to be associated with an event - -- @tparam[optchain] number metric A value to be associated with an event - -- @treturn nil - interface.track = function(key, user, data, metric) - local json = nil - - if data ~= nil then - json = toLaunchDarklyJSON(data) - end - - if metric ~= nil then - so.LDClientTrackMetric(client, key, user, json, metric) - else - so.LDClientTrack(client, key, user, json) - end - end - - --- Returns a map from feature flag keys to values for a given user. - -- This does not send analytics events back to LaunchDarkly. - -- @class function - -- @name allFlags - -- @tparam user user An opaque user object from @{makeUser} - -- @treturn table - interface.allFlags = function(user) - local x = so.LDAllFlags(client, user) - if x ~= nil then - return fromLaunchDarklyJSON(x) - else - return nil - end - end - - --- Evaluate a boolean flag - -- @class function - -- @name boolVariation - -- @tparam user user An opaque user object from @{makeUser} - -- @tparam string key The key of the flag to evaluate. - -- @tparam boolean fallback The value to return on error - -- @treturn boolean The evaluation result, or the fallback value - interface.boolVariation = function(user, key, fallback) - return so.LDBoolVariation(client, user, key, fallback, nil) - end - - --- Evaluate a boolean flag and return an explanation - -- @class function - -- @name boolVariationDetail - -- @tparam user user An opaque user object from @{makeUser} - -- @tparam string key The key of the flag to evaluate. - -- @tparam boolean fallback The value to return on error - -- @treturn table The evaluation explanation - interface.boolVariationDetail = function(user, key, fallback) - return genericVariationDetail(client, user, key, fallback, so.LDBoolVariation, nil) - end - - --- Evaluate an integer flag - -- @class function - -- @name intVariation - -- @tparam user user An opaque user object from @{makeUser} - -- @tparam string key The key of the flag to evaluate. - -- @tparam int fallback The value to return on error - -- @treturn int The evaluation result, or the fallback value - interface.intVariation = function(user, key, fallback) - return so.LDIntVariation(client, user, key, fallback, nil) - end - - --- Evaluate an integer flag and return an explanation - -- @class function - -- @name intVariationDetail - -- @tparam user user An opaque user object from @{makeUser} - -- @tparam string key The key of the flag to evaluate. - -- @tparam int fallback The value to return on error - -- @treturn table The evaluation explanation - interface.intVariationDetail = function(user, key, fallback) - return genericVariationDetail(client, user, key, fallback, so.LDIntVariation, nil) - end - - --- Evaluate a double flag - -- @class function - -- @name doubleVariation - -- @tparam user user An opaque user object from @{makeUser} - -- @tparam string key The key of the flag to evaluate. - -- @tparam number fallback The value to return on error - -- @treturn double The evaluation result, or the fallback value - interface.doubleVariation = function(user, key, fallback) - return so.LDDoubleVariation(client, user, key, fallback, nil) - end - - --- Evaluate a double flag and return an explanation - -- @class function - -- @name doubleVariationDetail - -- @tparam user user An opaque user object from @{makeUser} - -- @tparam string key The key of the flag to evaluate. - -- @tparam number fallback The value to return on error - -- @treturn table The evaluation explanation - interface.doubleVariationDetail = function(user, key, fallback) - return genericVariationDetail(client, user, key, fallback, so.LDDoubleVariation, nil) - end - - --- Evaluate a string flag - -- @class function - -- @name stringVariation - -- @tparam user user An opaque user object from @{makeUser} - -- @tparam string key The key of the flag to evaluate. - -- @tparam string fallback The value to return on error - -- @treturn string The evaluation result, or the fallback value - interface.stringVariation = function(user, key, fallback) - local raw = so.LDStringVariation(client, user, key, fallback, nil) - local native = ffi.string(raw) - so.LDFree(raw) - return native - end - - --- Evaluate a string flag and return an explanation - -- @class function - -- @name stringVariationDetail - -- @tparam user user An opaque user object from @{makeUser} - -- @tparam string key The key of the flag to evaluate. - -- @tparam string fallback The value to return on error - -- @treturn table The evaluation explanation - interface.stringVariationDetail = function(user, key, fallback) - local valueConverter = function(raw) - local native = ffi.string(raw) - so.LDFree(raw) - return native - end - - return genericVariationDetail(client, user, key, fallback, so.LDStringVariation, valueConverter) - end - - --- Evaluate a json flag - -- @class function - -- @name jsonVariation - -- @tparam user user An opaque user object from @{makeUser} - -- @tparam string key The key of the flag to evaluate. - -- @tparam table fallback The value to return on error - -- @treturn table The evaluation result, or the fallback value - interface.jsonVariation = function(user, key, fallback) - local raw = so.LDJSONVariation(client, user, key, toLaunchDarklyJSON(fallback), nil) - local native = fromLaunchDarklyJSON(raw) - so.LDJSONFree(raw) - return native - end - - --- Evaluate a json flag and return an explanation - -- @class function - -- @name jsonVariationDetail - -- @tparam user user An opaque user object from @{makeUser} - -- @tparam string key The key of the flag to evaluate. - -- @tparam table fallback The value to return on error - -- @treturn table The evaluation explanation - interface.jsonVariationDetail = function(user, key, fallback) - local valueConverter = function(raw) - local native = fromLaunchDarklyJSON(raw) - so.LDJSONFree(raw) - return native - end - - return genericVariationDetail(client, user, key, toLaunchDarklyJSON(fallback), so.LDJSONVariation, valueConverter) - end - - --- @type end - - return interface -end - ---- @export -return { - registerLogger = registerLogger, - makeUser = makeUser, - clientInit = clientInit -} diff --git a/test.lua b/test.lua index d8c267d..f23793d 100644 --- a/test.lua +++ b/test.lua @@ -1,6 +1,6 @@ local u = require('luaunit') -local l = require("launchdarkly-server-sdk") -local r = require("launchdarkly-server-sdk-redis") +local l = require("launchdarkly_server_sdk") +local r = require("launchdarkly_server_sdk_redis") function logger(level, line) print(level .. ": " .. line) @@ -27,27 +27,86 @@ end function TestAll:testBoolVariation() local e = false - u.assertEquals(e, makeTestClient().boolVariation(user, "test", e)) + u.assertEquals(e, makeTestClient():boolVariation(user, "test", e)) +end + +function TestAll:testBoolVariationDetail() + local e = { + value = true, + reason = { + kind = "ERROR", + errorKind = "CLIENT_NOT_READY" + } + } + u.assertEquals(e, makeTestClient():boolVariationDetail(user, "test", true)) end function TestAll:testIntVariation() local e = 3 - u.assertEquals(e, makeTestClient().intVariation(user, "test", e)) + u.assertEquals(e, makeTestClient():intVariation(user, "test", e)) +end + +function TestAll:testIntVariationDetail() + local e = { + value = 5, + reason = { + kind = "ERROR", + errorKind = "CLIENT_NOT_READY" + } + } + u.assertEquals(e, makeTestClient():intVariationDetail(user, "test", 5)) end function TestAll:testDoubleVariation() local e = 5.3 - u.assertEquals(e, makeTestClient().doubleVariation(user, "test", e)) + u.assertEquals(e, makeTestClient():doubleVariation(user, "test", e)) +end + +function TestAll:testDoubleVariationDetail() + local e = { + value = 6.2, + reason = { + kind = "ERROR", + errorKind = "CLIENT_NOT_READY" + } + } + u.assertEquals(e, makeTestClient():doubleVariationDetail(user, "test", 6.2)) end function TestAll:testStringVariation() local e = "a" - u.assertEquals(e, makeTestClient().stringVariation(user, "test", e)) + u.assertEquals(e, makeTestClient():stringVariation(user, "test", e)) +end + +function TestAll:testStringVariationDetail() + local e = { + value = "f", + reason = { + kind = "ERROR", + errorKind = "CLIENT_NOT_READY" + } + } + u.assertEquals(e, makeTestClient():stringVariationDetail(user, "test", "f")) end function TestAll:testJSONVariation() local e = { ["a"] = "b" } - u.assertEquals(e, makeTestClient().jsonVariation(user, "test", e)) + u.assertEquals(e, makeTestClient():jsonVariation(user, "test", e)) +end + +function TestAll:testJSONVariationDetail() + local e = { + value = { a = "b" }, + reason = { + kind = "ERROR", + errorKind = "CLIENT_NOT_READY" + } + } + u.assertEquals(e, makeTestClient():jsonVariationDetail(user, "test", { a = "b" })) +end + +function TestAll:testIdentify() + makeTestClient():identify(user) end function TestAll:testRedisBasic() @@ -59,7 +118,7 @@ function TestAll:testRedisBasic() local e = false - u.assertEquals(e, makeTestClient().boolVariation(user, "test", e)) + u.assertEquals(e, c:boolVariation(user, "test", e)) end local runner = u.LuaUnit.new() From 8a766384d90bdf9b362526ba2fb5e81a47b06652 Mon Sep 17 00:00:00 2001 From: hroederld Date: Mon, 20 Jul 2020 13:40:52 -0700 Subject: [PATCH 023/161] [ch83425] prepare beta 3 (#23) --- .ldrelease/update-version.sh | 2 +- README.md | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.ldrelease/update-version.sh b/.ldrelease/update-version.sh index 64d347a..02920ed 100755 --- a/.ldrelease/update-version.sh +++ b/.ldrelease/update-version.sh @@ -2,4 +2,4 @@ set -e -sed -i "s/local SDKVersion =.*/local SDKVersion = \"${LD_RELEASE_VERSION}\"/" 'launchdarkly-server-sdk.lua' +sed -i "s/#define SDKVersion .*/#define SDKVersion \"${LD_RELEASE_VERSION}\"/" 'launchdarkly-server-sdk.c' diff --git a/README.md b/README.md index 633d131..ce81f5f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ LaunchDarkly Server-Side SDK for Lua =========================== +*This version of the SDK is a **beta** version and should not be considered ready for production use while this message is visible.* + LaunchDarkly overview ------------------------- [LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves over 100 billion feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/docs/getting-started) using LaunchDarkly today! @@ -11,7 +13,7 @@ LaunchDarkly overview Supported Lua versions ----------- -This version of the LaunchDarkly SDK is compatible with the Lua 5.1 interpreter, and LuaJIT. Lua 5.3 is not supported due to FFI constraints. +This version of the LaunchDarkly SDK is compatible with the Lua 5.1-5.3 interpreter, and LuaJIT. Supported C server-side SDK versions ----------- From 368253a490954be9cf6dd083da78fd4b57e49016 Mon Sep 17 00:00:00 2001 From: hroederld Date: Mon, 27 Jul 2020 12:59:14 -0700 Subject: [PATCH 024/161] remove beta warning (#24) --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index ce81f5f..873d997 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ LaunchDarkly Server-Side SDK for Lua =========================== -*This version of the SDK is a **beta** version and should not be considered ready for production use while this message is visible.* - LaunchDarkly overview ------------------------- [LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves over 100 billion feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/docs/getting-started) using LaunchDarkly today! From fd7e7e3aa52bde9331a9a814e3f4ea7f01fafc4b Mon Sep 17 00:00:00 2001 From: hroederld Date: Mon, 27 Jul 2020 13:12:28 -0700 Subject: [PATCH 025/161] set source in rockspec (#25) --- launchdarkly-server-sdk-1.0-0.rockspec | 2 +- launchdarkly-server-sdk-redis-1.0-0.rockspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/launchdarkly-server-sdk-1.0-0.rockspec b/launchdarkly-server-sdk-1.0-0.rockspec index e58ed20..b3cd2b6 100644 --- a/launchdarkly-server-sdk-1.0-0.rockspec +++ b/launchdarkly-server-sdk-1.0-0.rockspec @@ -3,7 +3,7 @@ package = "launchdarkly-server-sdk" version = "1.0-0" source = { - url = "." -- not online yet! + url = "git+https://github.com/launchdarkly/lua-server-sdk.git" } dependencies = { diff --git a/launchdarkly-server-sdk-redis-1.0-0.rockspec b/launchdarkly-server-sdk-redis-1.0-0.rockspec index 1751204..07ef6b2 100644 --- a/launchdarkly-server-sdk-redis-1.0-0.rockspec +++ b/launchdarkly-server-sdk-redis-1.0-0.rockspec @@ -3,7 +3,7 @@ package = "launchdarkly-server-sdk-redis" version = "1.0-0" source = { - url = "." -- not online yet! + url = "git+https://github.com/launchdarkly/lua-server-sdk.git" } dependencies = { From 772a1035622e6906e7bb0edff2ae8f2c068b1621 Mon Sep 17 00:00:00 2001 From: hroederld Date: Fri, 28 Aug 2020 13:10:13 -0700 Subject: [PATCH 026/161] [ch87476] Always use custom `luaL_setfuncs` (#26) --- launchdarkly-server-sdk.c | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 2f3220a..2cf2ff8 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -1172,23 +1172,25 @@ static const struct luaL_Reg launchdarkly_store_methods[] = { { NULL, NULL } }; -#if !defined LUA_VERSION_NUM || LUA_VERSION_NUM==501 /* ** Adapted from Lua 5.2.0 */ -static void luaL_setfuncs (lua_State *L, const luaL_Reg *l, int nup) { - luaL_checkstack(L, nup+1, "too many upvalues"); - for (; l->name != NULL; l++) { /* fill the table with given functions */ - int i; - lua_pushstring(L, l->name); - for (i = 0; i < nup; i++) /* copy upvalues to the top */ - lua_pushvalue(L, -(nup+1)); - lua_pushcclosure(L, l->func, nup); /* closure with those upvalues */ - lua_settable(L, -(nup + 3)); - } - lua_pop(L, nup); /* remove upvalues */ +static void +ld_luaL_setfuncs(lua_State *L, const luaL_Reg *l, int nup) +{ + luaL_checkstack(L, nup+1, "too many upvalues"); + for (; l->name != NULL; l++) { /* fill the table with given functions */ + int i; + lua_pushstring(L, l->name); + /* copy upvalues to the top */ + for (i = 0; i < nup; i++) { + lua_pushvalue(L, -(nup+1)); + } + lua_pushcclosure(L, l->func, nup); /* closure with those upvalues */ + lua_settable(L, -(nup + 3)); + } + lua_pop(L, nup); /* remove upvalues */ } -#endif int luaopen_launchdarkly_server_sdk(lua_State *const l) @@ -1196,17 +1198,17 @@ luaopen_launchdarkly_server_sdk(lua_State *const l) luaL_newmetatable(l, "LaunchDarklyClient"); lua_pushvalue(l, -1); lua_setfield(l, -2, "__index"); - luaL_setfuncs(l, launchdarkly_client_methods, 0); + ld_luaL_setfuncs(l, launchdarkly_client_methods, 0); luaL_newmetatable(l, "LaunchDarklyUser"); lua_pushvalue(l, -1); lua_setfield(l, -2, "__index"); - luaL_setfuncs(l, launchdarkly_user_methods, 0); + ld_luaL_setfuncs(l, launchdarkly_user_methods, 0); luaL_newmetatable(l, "LaunchDarklyStoreInterface"); lua_pushvalue(l, -1); lua_setfield(l, -2, "__index"); - luaL_setfuncs(l, launchdarkly_store_methods, 0); + ld_luaL_setfuncs(l, launchdarkly_store_methods, 0); #if LUA_VERSION_NUM == 503 || LUA_VERSION_NUM == 502 luaL_newlib(l, launchdarkly_functions); From d248b2089cd402ef1e93e2d517be1796cfd2407a Mon Sep 17 00:00:00 2001 From: Ben Woskow <48036130+bwoskow-ld@users.noreply.github.com> Date: Fri, 20 Nov 2020 09:34:49 -0800 Subject: [PATCH 027/161] adding circleci badge and issue/PR templates (#27) --- .github/ISSUE_TEMPLATE/bug_report.md | 37 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++ .github/pull_request_template.md | 21 +++++++++++++ README.md | 4 ++- 4 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/pull_request_template.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..6b1d30d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,37 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Is this a support request?** +This issue tracker is maintained by LaunchDarkly SDK developers and is intended for feedback on the SDK code. If you're not sure whether the problem you are having is specifically related to the SDK, or to the LaunchDarkly service overall, it may be more appropriate to contact the LaunchDarkly support team; they can help to investigate the problem and will consult the SDK team if necessary. You can submit a support request by going [here](https://support.launchdarkly.com/) and clicking "submit a request", or by emailing support@launchdarkly.com. + +Note that issues filed on this issue tracker are publicly accessible. Do not provide any private account information on your issues. If your problem is specific to your account, you should submit a support request as described above. + +**Describe the bug** +A clear and concise description of what the bug is. + +**To reproduce** +Steps to reproduce the behavior. + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Logs** +If applicable, add any log output related to your problem. + +**SDK version** +The version of this SDK that you are using. + +**Language version, developer tools** +For instance, Go 1.11 or Ruby 2.5.3. If you are using a language that requires a separate compiler, such as C, please include the name and version of the compiler too. + +**OS/platform** +For instance, Ubuntu 16.04, Windows 10, or Android 4.0.3. If your code is running in a browser, please also include the browser type and version. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..3f7d5bf --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I would love to see the SDK [...does something new...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context about the feature request here. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..1980676 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,21 @@ +**Requirements** + +- [ ] I have added test coverage for new or changed functionality +- [ ] I have followed the repository's [pull request submission guidelines](../blob/master/CONTRIBUTING.md#submitting-pull-requests) +- [ ] I have validated my changes against all supported platform versions + +**Related issues** + +Provide links to any issues in this repository or elsewhere relating to this pull request. + +**Describe the solution you've provided** + +Provide a clear and concise description of what you expect to happen. + +**Describe alternatives you've considered** + +Provide a clear and concise description of any alternative solutions or features you've considered. + +**Additional context** + +Add any other context about the pull request here. diff --git a/README.md b/README.md index 873d997..5ce67d4 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,15 @@ LaunchDarkly Server-Side SDK for Lua =========================== +[![CircleCI](https://circleci.com/gh/launchdarkly/lua-server-sdk.svg?style=svg)](https://circleci.com/gh/launchdarkly/lua-server-sdk) + LaunchDarkly overview ------------------------- + [LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves over 100 billion feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/docs/getting-started) using LaunchDarkly today! [![Twitter Follow](https://img.shields.io/twitter/follow/launchdarkly.svg?style=social&label=Follow&maxAge=2592000)](https://twitter.com/intent/follow?screen_name=launchdarkly) - Supported Lua versions ----------- From 6243cfba234641ab546bf7001cbcd6b09812055b Mon Sep 17 00:00:00 2001 From: hroederld Date: Wed, 3 Feb 2021 12:20:46 -0800 Subject: [PATCH 028/161] [ch99754] add alias method (#28) --- launchdarkly-server-sdk.c | 30 ++++++++++++++++++++++++++++++ test.lua | 4 ++++ 2 files changed, 34 insertions(+) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 18d2a95..f146289 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -1061,6 +1061,35 @@ LuaLDClientTrack(lua_State *const l) return 0; } +/*** +Associates two users for analytics purposes by generating an alias event. +@function alias +@tparam user currentUser An opaque user object from @{makeUser} +@tparam user previousUser An opaque user object from @{makeUser} +@treturn nil +*/ +static int +LuaLDClientAlias(lua_State *const l) +{ + struct LDClient **client; + struct LDUser **currentUser, **previousUser; + struct LDJSON *value; + + if (lua_gettop(l) != 3) { + return luaL_error(l, "expected exactly three arguments"); + } + + client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + currentUser = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); + previousUser = (struct LDUser **)luaL_checkudata(l, 3, "LaunchDarklyUser"); + + if (!LDClientAlias(*client, *currentUser, *previousUser)) { + return luaL_error(l, "LDClientAlias failed"); + } + + return 0; +} + /*** Check if a client has been fully initialized. This may be useful if the initialization timeout was reached. @@ -1156,6 +1185,7 @@ static const struct luaL_Reg launchdarkly_client_methods[] = { { "jsonVariationDetail", LuaLDClientJSONVariationDetail }, { "flush", LuaLDClientFlush }, { "track", LuaLDClientTrack }, + { "alias", LuaLDClientAlias }, { "allFlags", LuaLDClientAllFlags }, { "isInitialized", LuaLDClientIsInitialized }, { "identify", LuaLDClientIdentify }, diff --git a/test.lua b/test.lua index f23793d..55c129b 100644 --- a/test.lua +++ b/test.lua @@ -109,6 +109,10 @@ function TestAll:testIdentify() makeTestClient():identify(user) end +function TestAll:testAlias() + makeTestClient():alias(user, l.makeUser({ key = "bob" })) +end + function TestAll:testRedisBasic() local c = l.clientInit({ key = "sdk-test", From 4b43131a0fcda9f77a1e8a298a87cfb0a52fa40a Mon Sep 17 00:00:00 2001 From: Ben Woskow <48036130+bwoskow-ld@users.noreply.github.com> Date: Wed, 3 Feb 2021 15:16:08 -0800 Subject: [PATCH 029/161] Removed the guides link --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 5ce67d4..02d7ea6 100644 --- a/README.md +++ b/README.md @@ -54,4 +54,3 @@ About LaunchDarkly * [docs.launchdarkly.com](https://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and SDK reference guides * [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/ "LaunchDarkly API Documentation") for our API documentation * [blog.launchdarkly.com](https://blog.launchdarkly.com/ "LaunchDarkly Blog Documentation") for the latest product updates - * [Feature Flagging Guide](https://github.com/launchdarkly/featureflags/ "Feature Flagging Guide") for best practices and strategies From 9006383c76722178c538d6aa6e9b73c70d2c7578 Mon Sep 17 00:00:00 2001 From: ember-stevens <79482775+ember-stevens@users.noreply.github.com> Date: Wed, 17 Nov 2021 13:00:27 -0800 Subject: [PATCH 030/161] Updates docs URLs (#29) --- CONTRIBUTING.md | 2 +- README.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 51ae91c..86266f3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1,7 @@ Contributing to the LaunchDarkly Server-Side SDK for Lua ================================================ -LaunchDarkly has published an [SDK contributor's guide](https://docs.launchdarkly.com/docs/sdk-contributors-guide) that provides a detailed explanation of how our SDKs work. See below for additional information on how to contribute to this SDK. +LaunchDarkly has published an [SDK contributor's guide](https://docs.launchdarkly.com/sdk/concepts/contributors-guide) that provides a detailed explanation of how our SDKs work. See below for additional information on how to contribute to this SDK. Submitting bug reports and feature requests ------------------ diff --git a/README.md b/README.md index 02d7ea6..3f01ae2 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ LaunchDarkly Server-Side SDK for Lua LaunchDarkly overview ------------------------- -[LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves over 100 billion feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/docs/getting-started) using LaunchDarkly today! +[LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves over 100 billion feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/home/getting-started) using LaunchDarkly today! [![Twitter Follow](https://img.shields.io/twitter/follow/launchdarkly.svg?style=social&label=Follow&maxAge=2592000)](https://twitter.com/intent/follow?screen_name=launchdarkly) @@ -48,9 +48,9 @@ About LaunchDarkly * Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?). * Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file. * Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). Disable parts of your application to facilitate maintenance, without taking everything offline. -* LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Check out [our documentation](https://docs.launchdarkly.com/docs) for a complete list. +* LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Read [our documentation](https://docs.launchdarkly.com/sdk) for a complete list. * Explore LaunchDarkly * [launchdarkly.com](https://www.launchdarkly.com/ "LaunchDarkly Main Website") for more information * [docs.launchdarkly.com](https://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and SDK reference guides * [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/ "LaunchDarkly API Documentation") for our API documentation - * [blog.launchdarkly.com](https://blog.launchdarkly.com/ "LaunchDarkly Blog Documentation") for the latest product updates + * [launchdarkly.com/blog](https://launchdarkly.com/blog/ "LaunchDarkly Blog Documentation") for the latest product updates From d03089f9ab141dc96c175a82c881f7165a26bb97 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 19 Nov 2021 11:06:03 -0800 Subject: [PATCH 031/161] SC-127348: Add LuaLDVersion function to return current SDK version (#30) * Add LuaLDVersion to return current SDK version --- launchdarkly-server-sdk.c | 14 ++++++++++++++ test.lua | 6 ++++++ 2 files changed, 20 insertions(+) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 47257c4..31e528d 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -358,6 +358,19 @@ LuaLDUserNew(lua_State *const l) return 1; } + +/*** +Return SDK version. +@function version +@return SDK version string. +*/ +static int +LuaLDVersion(lua_State *const l) +{ + lua_pushstring(l, SDKVersion); + return 1; +} + static int LuaLDUserFree(lua_State *const l) { @@ -1169,6 +1182,7 @@ static const struct luaL_Reg launchdarkly_functions[] = { { "clientInit", LuaLDClientInit }, { "makeUser", LuaLDUserNew }, { "registerLogger", LuaLDRegisterLogger }, + { "version", LuaLDVersion }, { NULL, NULL } }; diff --git a/test.lua b/test.lua index 55c129b..f6344f0 100644 --- a/test.lua +++ b/test.lua @@ -125,5 +125,11 @@ function TestAll:testRedisBasic() u.assertEquals(e, c:boolVariation(user, "test", e)) end +function TestAll:testVersion() + local version = l.version() + u.assertNotIsNil(version) + u.assertStrMatches(version, "(%d+)%.(%d+)%.(%d+)(.*)") +end + local runner = u.LuaUnit.new() os.exit(runner:runSuite()) From 78eadf387cdf275d53e1e5916a039b02203ab2fe Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 27 Jan 2022 00:23:29 -0800 Subject: [PATCH 032/161] Remove references to unneeded lua-cjson package (#31) Additionally, remove redundant references to zip package. --- .circleci/config.yml | 6 +++--- .ldrelease/linux-prepare.sh | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 8d7d012..c5b17d8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -20,7 +20,7 @@ jobs: command: | apt-get update -y apt-get install -y luajit libluajit-5.1-dev lua-ldoc zip \ - ca-certificates curl zip lua-cjson libpcre3 libcurl4-openssl-dev \ + ca-certificates curl libpcre3 libcurl4-openssl-dev \ cmake libhiredis-dev git build-essential libpcre3-dev luarocks - run: @@ -49,7 +49,7 @@ jobs: command: | apt-get update -y apt-get install -y lua5.3 liblua5.3-dev lua-ldoc zip \ - ca-certificates curl zip lua-cjson libpcre3 libcurl4-openssl-dev \ + ca-certificates curl libpcre3 libcurl4-openssl-dev \ cmake libhiredis-dev git build-essential libpcre3-dev luarocks - run: @@ -78,7 +78,7 @@ jobs: command: | apt-get update -y apt-get install -y lua5.2 liblua5.2-dev lua-ldoc zip \ - ca-certificates curl zip lua-cjson libpcre3 libcurl4-openssl-dev \ + ca-certificates curl libpcre3 libcurl4-openssl-dev \ cmake libhiredis-dev git build-essential libpcre3-dev luarocks - run: diff --git a/.ldrelease/linux-prepare.sh b/.ldrelease/linux-prepare.sh index 1cdb702..1df986f 100755 --- a/.ldrelease/linux-prepare.sh +++ b/.ldrelease/linux-prepare.sh @@ -3,5 +3,5 @@ set -e apt-get update -y && apt-get install -y luajit lua-ldoc zip ca-certificates \ - curl zip lua-cjson libpcre3 libcurl4-openssl-dev cmake libhiredis-dev git \ + curl libpcre3 libcurl4-openssl-dev cmake libhiredis-dev git \ build-essential libpcre3-dev From d5b97c9bb66e79559af26445697bf8619eddf8be Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 4 Feb 2022 14:46:28 -0800 Subject: [PATCH 033/161] Fix memory leak in AllFlags (#32) Add LDJSONFree to AllFlags to resolve un-freed JSON object. --- launchdarkly-server-sdk.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 31e528d..767fad0 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -1175,6 +1175,8 @@ LuaLDClientAllFlags(lua_State *const l) LuaPushJSON(l, result); + LDJSONFree(result); + return 1; } From 619604ae2d340fc98bd855a47441b8eb64df9514 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 4 Feb 2022 16:17:23 -0800 Subject: [PATCH 034/161] Update to releaser v2 config (#33) --- .ldrelease/config.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.ldrelease/config.yml b/.ldrelease/config.yml index 32e5ac5..2656340 100644 --- a/.ldrelease/config.yml +++ b/.ldrelease/config.yml @@ -1,14 +1,17 @@ +version: 2 + repo: public: lua-server-sdk private: lua-server-sdk-private -circleci: - linux: - image: ubuntu:18.04 +jobs: +- circleCI: + linux: + image: ubuntu:18.04 documentation: title: LaunchDarkly Server-Side SDK for Lua - githubPages: true + gitHubPages: true sdk: displayName: "Lua (server-side)" From bc02168d5c013da69c8fa801a50dd63d2713de31 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 4 Feb 2022 16:27:15 -0800 Subject: [PATCH 035/161] Add .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..485dee6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea From 46a456e3ba364fd26f41d0b0d1e29ce6a228bb2d Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 8 Feb 2022 10:09:14 -0800 Subject: [PATCH 036/161] Fix documentation generation for Releaser v2 config (#34) --- .ldrelease/linux-build-docs.sh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.ldrelease/linux-build-docs.sh b/.ldrelease/linux-build-docs.sh index d2c3f2f..9b3ae6a 100755 --- a/.ldrelease/linux-build-docs.sh +++ b/.ldrelease/linux-build-docs.sh @@ -12,6 +12,4 @@ cp launchdarkly-server-sdk-redis.c tmp/ ldoc tmp -mkdir -p $PROJECT_DIR/artifacts -cd $PROJECT_DIR/doc -zip -r $PROJECT_DIR/artifacts/docs.zip * +cp -r ${PROJECT_DIR}/docs/* ${LD_RELEASE_DOCS_DIR} From 34a9eef3191bedf97ad65663dea5fcb46bd36ebd Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 8 Feb 2022 11:21:11 -0800 Subject: [PATCH 037/161] Fix Releaser v2 documentation generation & optimize build (#35) This commit fixes documentation generation by having ldoc directly write to the LD_RELEASE_DOCS_DIR path. It was previously broken because it was attempting to copy from PROJECT_DIR/docs to LD_RELEASE_DOCS_DIR, but ldoc actually creates a folder named "doc" by default (typo on my part.) Additionally, since there is no need to use CircleCI for Releaser builds, we can simply replace it with Releaser's docker functionality. This makes the build more stable since Releaser doesn't need to deal with CircleCI. --- .ldrelease/{linux-build-docs.sh => build-docs.sh} | 6 +----- .ldrelease/config.yml | 3 +-- .ldrelease/{linux-prepare.sh => prepare.sh} | 0 3 files changed, 2 insertions(+), 7 deletions(-) rename .ldrelease/{linux-build-docs.sh => build-docs.sh} (70%) rename .ldrelease/{linux-prepare.sh => prepare.sh} (100%) diff --git a/.ldrelease/linux-build-docs.sh b/.ldrelease/build-docs.sh similarity index 70% rename from .ldrelease/linux-build-docs.sh rename to .ldrelease/build-docs.sh index 9b3ae6a..9db4ea7 100755 --- a/.ldrelease/linux-build-docs.sh +++ b/.ldrelease/build-docs.sh @@ -4,12 +4,8 @@ set -e # This only runs in the Linux build, since the docs are the same for all platforms. -PROJECT_DIR=$(pwd) - mkdir tmp cp launchdarkly-server-sdk.c tmp/ cp launchdarkly-server-sdk-redis.c tmp/ -ldoc tmp - -cp -r ${PROJECT_DIR}/docs/* ${LD_RELEASE_DOCS_DIR} +ldoc -d ${LD_RELEASE_DOCS_DIR} tmp diff --git a/.ldrelease/config.yml b/.ldrelease/config.yml index 2656340..74f2fbe 100644 --- a/.ldrelease/config.yml +++ b/.ldrelease/config.yml @@ -5,8 +5,7 @@ repo: private: lua-server-sdk-private jobs: -- circleCI: - linux: + - docker: image: ubuntu:18.04 documentation: diff --git a/.ldrelease/linux-prepare.sh b/.ldrelease/prepare.sh similarity index 100% rename from .ldrelease/linux-prepare.sh rename to .ldrelease/prepare.sh From b50485b92be46daef0f162bf881b48074123632a Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 8 Feb 2022 13:28:41 -0800 Subject: [PATCH 038/161] Remove tmp dir (#36) This dir was checked in by Releaser. Removing by utilizing releaser's TEMP_DIR. --- .ldrelease/build-docs.sh | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.ldrelease/build-docs.sh b/.ldrelease/build-docs.sh index 9db4ea7..bbafab2 100755 --- a/.ldrelease/build-docs.sh +++ b/.ldrelease/build-docs.sh @@ -4,8 +4,11 @@ set -e # This only runs in the Linux build, since the docs are the same for all platforms. -mkdir tmp -cp launchdarkly-server-sdk.c tmp/ -cp launchdarkly-server-sdk-redis.c tmp/ +# LD_RELEASE_TEMP_DIR is guaranteed to be empty, and will not be checked into version control. +DOCS_BUILD_DIR="$LD_RELEASE_TEMP_DIR/docs" +mkdir "$DOCS_BUILD_DIR" -ldoc -d ${LD_RELEASE_DOCS_DIR} tmp +cp launchdarkly-server-sdk.c "$DOCS_BUILD_DIR" +cp launchdarkly-server-sdk-redis.c "$DOCS_BUILD_DIR" + +ldoc -d "$LD_RELEASE_DOCS_DIR" "$DOCS_BUILD_DIR" From 1904c533009e2749ec4e1580eedbb8a6ad1868ac Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 8 Feb 2022 13:31:43 -0800 Subject: [PATCH 039/161] Remove tmp dir (continued) Previous commit removed the code that created the tmp dir. This commit removes the actual code from the source. --- tmp/launchdarkly-server-sdk-redis.c | 110 --- tmp/launchdarkly-server-sdk.c | 1266 --------------------------- 2 files changed, 1376 deletions(-) delete mode 100644 tmp/launchdarkly-server-sdk-redis.c delete mode 100644 tmp/launchdarkly-server-sdk.c diff --git a/tmp/launchdarkly-server-sdk-redis.c b/tmp/launchdarkly-server-sdk-redis.c deleted file mode 100644 index 606b63b..0000000 --- a/tmp/launchdarkly-server-sdk-redis.c +++ /dev/null @@ -1,110 +0,0 @@ -/*** -Server-side SDK for LaunchDarkly Redis store. -@module launchdarkly-server-sdk-redis -*/ - -#include -#include -#include -#include -#include - -#include -#include - -/*** -Initialize a store backend -@function makeStore -@tparam table fields list of configuration options -@tparam[opt] string fields.host Hostname for Redis. -@tparam[opt] int fields.port Port for Redis. -@tparam[opt] string fields.prefix Redis key prefix for SDK values. -@tparam[opt] int fields.poolSize Number of Redis connections to maintain. -@return A fresh Redis store backend. -*/ -static int -LuaLDRedisMakeStore(lua_State *const l) -{ - struct LDRedisConfig *config; - struct LDStoreInterface *storeInterface; - - if (lua_gettop(l) != 1) { - return luaL_error(l, "expecting exactly 1 argument"); - } - - luaL_checktype(l, 1, LUA_TTABLE); - - config = LDRedisConfigNew(); - - lua_getfield(l, 1, "host"); - - if (lua_isstring(l, -1)) { - LDRedisConfigSetHost(config, luaL_checkstring(l, -1)); - } - - lua_getfield(l, 1, "prefix"); - - if (lua_isstring(l, -1)) { - LDRedisConfigSetPrefix(config, luaL_checkstring(l, -1)); - } - - lua_getfield(l, 1, "port"); - - if (lua_isnumber(l, -1)) { - LDRedisConfigSetPort(config, luaL_checkinteger(l, -1)); - } - - lua_getfield(l, 1, "poolSize"); - - if (lua_isnumber(l, -1)) { - LDRedisConfigSetPoolSize(config, luaL_checkinteger(l, -1)); - } - - storeInterface = LDStoreInterfaceRedisNew(config); - - struct LDStoreInterface **i = - (struct LDStoreInterface **)lua_newuserdata(l, sizeof(storeInterface)); - - *i = storeInterface; - - luaL_getmetatable(l, "LaunchDarklyStoreInterface"); - lua_setmetatable(l, -2); - - return 1; -} - -static const struct luaL_Reg launchdarkly_functions[] = { - { "makeStore", LuaLDRedisMakeStore }, - { NULL, NULL } -}; - -#if !defined LUA_VERSION_NUM || LUA_VERSION_NUM==501 -/* -** Adapted from Lua 5.2.0 -*/ -static void luaL_setfuncs (lua_State *L, const luaL_Reg *l, int nup) { - luaL_checkstack(L, nup+1, "too many upvalues"); - for (; l->name != NULL; l++) { /* fill the table with given functions */ - int i; - lua_pushstring(L, l->name); - for (i = 0; i < nup; i++) /* copy upvalues to the top */ - lua_pushvalue(L, -(nup+1)); - lua_pushcclosure(L, l->func, nup); /* closure with those upvalues */ - lua_settable(L, -(nup + 3)); - } - lua_pop(L, nup); /* remove upvalues */ -} -#endif - -int -luaopen_launchdarkly_server_sdk_redis(lua_State *const l) -{ - #if LUA_VERSION_NUM == 503 || LUA_VERSION_NUM == 502 - luaL_newlib(l, launchdarkly_functions); - #else - luaL_register(l, "launchdarkly-server-sdk-redis", - launchdarkly_functions); - #endif - - return 1; -} diff --git a/tmp/launchdarkly-server-sdk.c b/tmp/launchdarkly-server-sdk.c deleted file mode 100644 index 2b44fc8..0000000 --- a/tmp/launchdarkly-server-sdk.c +++ /dev/null @@ -1,1266 +0,0 @@ -/*** -Server-side SDK for LaunchDarkly. -@module launchdarkly-server-sdk -*/ - -#include -#include -#include -#include -#include - -#include - -#define SDKVersion "1.2.1" - -static struct LDJSON * -LuaValueToJSON(lua_State *const l, const int i); - -static struct LDJSON * -LuaTableToJSON(lua_State *const l, const int i); - -static struct LDJSON * -LuaArrayToJSON(lua_State *const l, const int i); - -static void -LuaPushJSON(lua_State *const l, const struct LDJSON *const j); - -static int globalLoggingCallback; -static lua_State *globalLuaState; - -static void -logHandler(const LDLogLevel level, const char * const line) -{ - lua_rawgeti(globalLuaState, LUA_REGISTRYINDEX, globalLoggingCallback); - - lua_pushstring(globalLuaState, LDLogLevelToString(level)); - lua_pushstring(globalLuaState, line); - - lua_call(globalLuaState, 2, 0); -} - -static LDLogLevel -LuaStringToLogLevel(const char *const text) -{ - if (strcmp(text, "FATAL") == 0) { - return LD_LOG_FATAL; - } else if (strcmp(text, "CRITICAL") == 0) { - return LD_LOG_CRITICAL; - } else if (strcmp(text, "ERROR") == 0) { - return LD_LOG_ERROR; - } else if (strcmp(text, "WARNING") == 0) { - return LD_LOG_WARNING; - } else if (strcmp(text, "INFO") == 0) { - return LD_LOG_INFO; - } else if (strcmp(text, "DEBUG") == 0) { - return LD_LOG_DEBUG; - } else if (strcmp(text, "TRACE") == 0) { - return LD_LOG_TRACE; - } - - return LD_LOG_INFO; -} - -/*** -Set the global logger for all SDK operations. This function is not thread -safe, and if used should be done so before other operations. The default -log level is "INFO". -@function registerLogger -@tparam string logLevel The level to at. Available options are: -"FATAL", "CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "TRACE". -@tparam function cb The logging handler. Callback must be of the form -"function (logLevel, logLine) ... end". -*/ -static int -LuaLDRegisterLogger(lua_State *const l) -{ - if (lua_gettop(l) != 2) { - return luaL_error(l, "expecting exactly 2 arguments"); - } - - const char *const level = luaL_checkstring(l, 1); - - globalLuaState = l; - globalLoggingCallback = luaL_ref(l, LUA_REGISTRYINDEX); - - LDConfigureGlobalLogger(LuaStringToLogLevel(level), logHandler); - - return 0; -} - -static bool -isArray(lua_State *const l, const int i) -{ - lua_pushvalue(l, i); - - lua_pushnil(l); - - bool array = true; - - while (lua_next(l, -2) != 0) { - if (lua_type(l, -2) != LUA_TNUMBER) { - array = false; - } - - lua_pop(l, 1); - } - - lua_pop(l, 1); - - return array; -} - -static struct LDJSON * -LuaValueToJSON(lua_State *const l, const int i) -{ - struct LDJSON *result; - - switch (lua_type(l, i)) { - case LUA_TBOOLEAN: - result = LDNewBool(lua_toboolean(l, i)); - break; - case LUA_TNUMBER: - result = LDNewNumber(lua_tonumber(l, i)); - break; - case LUA_TSTRING: - result = LDNewText(lua_tostring(l, i)); - break; - case LUA_TTABLE: - if (isArray(l, i)) { - result = LuaArrayToJSON(l, i); - } else { - result = LuaTableToJSON(l, i); - } - break; - default: - result = LDNewNull(); - break; - } - - return result; -} - -static struct LDJSON * -LuaArrayToJSON(lua_State *const l, const int i) -{ - struct LDJSON *const result = LDNewArray(); - - lua_pushvalue(l, i); - - lua_pushnil(l); - - while (lua_next(l, -2) != 0) { - struct LDJSON *value = LuaValueToJSON(l, -1); - - LDArrayPush(result, value); - - lua_pop(l, 1); - } - - lua_pop(l, 1); - - return result; -} - -static struct LDJSON * -LuaTableToJSON(lua_State *const l, const int i) -{ - struct LDJSON *const result = LDNewObject(); - - lua_pushvalue(l, i); - - lua_pushnil(l); - - while (lua_next(l, -2) != 0) { - const char *const key = lua_tostring(l, -2); - struct LDJSON *const value = LuaValueToJSON(l, -1); - - LDObjectSetKey(result, key, value); - - lua_pop(l, 1); - } - - lua_pop(l, 1); - - return result; -} - -static void -LuaPushJSONObject(lua_State *const l, const struct LDJSON *const j) -{ - struct LDJSON *iter; - - lua_newtable(l); - - for (iter = LDGetIter(j); iter; iter = LDIterNext(iter)) { - LuaPushJSON(l, iter); - lua_setfield(l, -2, LDIterKey(iter)); - } -} - -static void -LuaPushJSONArray(lua_State *const l, const struct LDJSON *const j) -{ - struct LDJSON *iter; - - lua_newtable(l); - - int index = 1; - - for (iter = LDGetIter(j); iter; iter = LDIterNext(iter)) { - LuaPushJSON(l, iter); - lua_rawseti(l, -2, index); - index++; - } -} - -static void -LuaPushJSON(lua_State *const l, const struct LDJSON *const j) -{ - switch (LDJSONGetType(j)) { - case LDText: - lua_pushstring(l, LDGetText(j)); - break; - case LDBool: - lua_pushboolean(l, LDGetBool(j)); - break; - case LDNumber: - lua_pushnumber(l, LDGetNumber(j)); - break; - case LDObject: - LuaPushJSONObject(l, j); - break; - case LDArray: - LuaPushJSONArray(l, j); - break; - default: - lua_pushnil(l); - break; - } - - return; -} - -/*** -Create a new opaque user object. -@function makeUser -@tparam table fields list of user fields. -@tparam string fields.key The user's key -@tparam[opt] boolean fields.anonymous Mark the user as anonymous -@tparam[opt] string fields.ip Set the user's IP -@tparam[opt] string fields.firstName Set the user's first name -@tparam[opt] string fields.lastName Set the user's last name -@tparam[opt] string fields.email Set the user's email -@tparam[opt] string fields.name Set the user's name -@tparam[opt] string fields.avatar Set the user's avatar -@tparam[opt] string fields.country Set the user's country -@tparam[opt] string fields.secondary Set the user's secondary key -@tparam[opt] table fields.privateAttributeNames A list of attributes to -redact -@tparam[opt] table fields.custom Set the user's custom JSON -@return an opaque user object -*/ -static int -LuaLDUserNew(lua_State *const l) -{ - if (lua_gettop(l) != 1) { - return luaL_error(l, "expecting exactly 1 argument"); - } - - luaL_checktype(l, 1, LUA_TTABLE); - - lua_getfield(l, 1, "key"); - - const char *const key = luaL_checkstring(l, -1); - - struct LDUser *user = LDUserNew(key); - - lua_getfield(l, 1, "anonymous"); - - if (lua_isboolean(l, -1)) { - LDUserSetAnonymous(user, lua_toboolean(l, -1)); - } - - lua_getfield(l, 1, "ip"); - - if (lua_isstring(l, -1)) { - LDUserSetIP(user, luaL_checkstring(l,-1)); - }; - - lua_getfield(l, 1, "firstName"); - - if (lua_isstring(l, -1)) { - LDUserSetFirstName(user, luaL_checkstring(l,-1)); - } - - lua_getfield(l, 1, "lastName"); - - if (lua_isstring(l, -1)) { - LDUserSetLastName(user, luaL_checkstring(l, -1)); - } - - lua_getfield(l, 1, "email"); - - if (lua_isstring(l, -1)) { - LDUserSetEmail(user, luaL_checkstring(l, -1)); - } - - lua_getfield(l, 1, "name"); - - if (lua_isstring(l, -1)) { - LDUserSetName(user, luaL_checkstring(l, -1)); - } - - lua_getfield(l, 1, "avatar"); - - if (lua_isstring(l, -1)) { - LDUserSetAvatar(user, luaL_checkstring(l, -1)); - } - - lua_getfield(l, 1, "country"); - - if (lua_isstring(l, -1)) { - LDUserSetCountry(user, luaL_checkstring(l, -1)); - } - - lua_getfield(l, 1, "secondary"); - - if (lua_isstring(l, -1)) { - LDUserSetSecondary(user, luaL_checkstring(l, -1)); - } - - lua_getfield(l, 1, "custom"); - - if (lua_istable(l, -1)) { - LDUserSetCustom(user, LuaValueToJSON(l, -1)); - } - - lua_getfield(l, 1, "privateAttributeNames"); - - if (lua_istable(l, -1)) { - struct LDJSON *attrs, *iter; - attrs = LuaValueToJSON(l, -1); - - for (iter = LDGetIter(attrs); iter; iter = LDIterNext(iter)) { - LDUserAddPrivateAttribute(user, LDGetText(iter)); - } - - LDJSONFree(attrs); - } - - struct LDUser **u = (struct LDUser **)lua_newuserdata(l, sizeof(user)); - - *u = user; - - luaL_getmetatable(l, "LaunchDarklyUser"); - lua_setmetatable(l, -2); - - return 1; -} - - -/*** -Return SDK version. -@function version -@return SDK version string. -*/ -static int -LuaLDVersion(lua_State *const l) -{ - lua_pushstring(l, SDKVersion); - return 1; -} - -static int -LuaLDUserFree(lua_State *const l) -{ - struct LDUser **user; - - user = (struct LDUser **)luaL_checkudata(l, 1, "LaunchDarklyUser"); - - if (*user) { - LDUserFree(*user); - *user = NULL; - } - - return 0; -} - -static struct LDConfig * -makeConfig(lua_State *const l, const int i) -{ - struct LDConfig *config; - - luaL_checktype(l, i, LUA_TTABLE); - - lua_getfield(l, i, "key"); - - const char *const key = luaL_checkstring(l, -1); - - config = LDConfigNew(key); - - lua_getfield(l, i, "baseURI"); - - if (lua_isstring(l, -1)) { - LDConfigSetBaseURI(config, luaL_checkstring(l, -1)); - } - - lua_getfield(l, i, "streamURI"); - - if (lua_isstring(l, -1)) { - LDConfigSetStreamURI(config, luaL_checkstring(l, -1)); - } - - lua_getfield(l, i, "eventsURI"); - - if (lua_isstring(l, -1)) { - LDConfigSetEventsURI(config, luaL_checkstring(l, -1)); - } - - lua_getfield(l, i, "stream"); - - if (lua_isboolean(l, -1)) { - LDConfigSetStream(config, lua_toboolean(l, -1)); - } - - lua_getfield(l, i, "sendEvents"); - - if (lua_isstring(l, -1)) { - LDConfigSetSendEvents(config, lua_toboolean(l, -1)); - } - - lua_getfield(l, i, "eventsCapacity"); - - if (lua_isnumber(l, -1)) { - LDConfigSetEventsCapacity(config, luaL_checkinteger(l, -1)); - } - - lua_getfield(l, i, "timeout"); - - if (lua_isnumber(l, -1)) { - LDConfigSetTimeout(config, luaL_checkinteger(l, -1)); - } - - lua_getfield(l, i, "flushInterval"); - - if (lua_isnumber(l, -1)) { - LDConfigSetFlushInterval(config, luaL_checkinteger(l, -1)); - } - - lua_getfield(l, i, "pollInterval"); - - if (lua_isnumber(l, -1)) { - LDConfigSetPollInterval(config, luaL_checkinteger(l, -1)); - } - - lua_getfield(l, i, "offline"); - - if (lua_isboolean(l, -1)) { - LDConfigSetOffline(config, lua_toboolean(l, -1)); - } - - lua_getfield(l, i, "useLDD"); - - if (lua_isboolean(l, -1)) { - LDConfigSetUseLDD(config, lua_toboolean(l, -1)); - } - - lua_getfield(l, i, "inlineUsersInEvents"); - - if (lua_isboolean(l, -1)) { - LDConfigInlineUsersInEvents(config, lua_toboolean(l, -1)); - } - - lua_getfield(l, i, "allAttributesPrivate"); - - if (lua_isboolean(l, -1)) { - LDConfigSetAllAttributesPrivate(config, lua_toboolean(l, -1)); - } - - lua_getfield(l, i, "userKeysCapacity"); - - if (lua_isnumber(l, -1)) { - LDConfigSetUserKeysCapacity(config, luaL_checkinteger(l, -1)); - } - - lua_getfield(l, i, "featureStoreBackend"); - - if (lua_isuserdata(l, -1)) { - struct LDStoreInterface **storeInterface; - - storeInterface = (struct LDStoreInterface **) - luaL_checkudata(l, -1, "LaunchDarklyStoreInterface"); - - LDConfigSetFeatureStoreBackend(config, *storeInterface); - } - - lua_getfield(l, 1, "privateAttributeNames"); - - if (lua_istable(l, -1)) { - struct LDJSON *attrs, *iter; - attrs = LuaValueToJSON(l, -1); - - for (iter = LDGetIter(attrs); iter; iter = LDIterNext(iter)) { - LDConfigAddPrivateAttribute(config, LDGetText(iter)); - } - - LDJSONFree(attrs); - } - - LDConfigSetWrapperInfo(config, "lua-server-sdk", SDKVersion); - - return config; -} - -/*** -Initialize a new client, and connect to LaunchDarkly. -@function makeClient -@tparam table config list of configuration options -@tparam string config.key Environment SDK key -@tparam[opt] string config.baseURI Set the base URI for connecting to -LaunchDarkly. You probably don't need to set this unless instructed by -LaunchDarkly. -@tparam[opt] string config.streamURI Set the streaming URI for connecting to -LaunchDarkly. You probably don't need to set this unless instructed by -LaunchDarkly. -@tparam[opt] string config.eventsURI Set the events URI for connecting to -LaunchDarkly. You probably don't need to set this unless instructed by -LaunchDarkly. -@tparam[opt] boolean config.stream Enables or disables real-time streaming -flag updates. When set to false, an efficient caching polling mechanism is -used. We do not recommend disabling streaming unless you have been instructed -to do so by LaunchDarkly support. Defaults to true. -@tparam[opt] string config.sendEvents Sets whether to send analytics events -back to LaunchDarkly. By default, the client will send events. This differs -from Offline in that it only affects sending events, not streaming or -polling. -@tparam[opt] int config.eventsCapacity The capacity of the events buffer. -The client buffers up to this many events in memory before flushing. If the -capacity is exceeded before the buffer is flushed, events will be discarded. -@tparam[opt] int config.timeout The connection timeout to use when making -requests to LaunchDarkly. -@tparam[opt] int config.flushInterval he time between flushes of the event -buffer. Decreasing the flush interval means that the event buffer is less -likely to reach capacity. -@tparam[opt] int config.pollInterval The polling interval -(when streaming is disabled) in milliseconds. -@tparam[opt] boolean config.offline Sets whether this client is offline. -An offline client will not make any network connections to LaunchDarkly, -and will return default values for all feature flags. -@tparam[opt] boolean config.allAttributesPrivate Sets whether or not all user -attributes (other than the key) should be hidden from LaunchDarkly. If this -is true, all user attribute values will be private, not just the attributes -specified in PrivateAttributeNames. -@tparam[opt] boolean config.inlineUsersInEvents Set to true if you need to -see the full user details in every analytics event. -@tparam[opt] int config.userKeysCapacity The number of user keys that the -event processor can remember at an one time, so that duplicate user details -will not be sent in analytics. -@tparam[opt] int config.userKeysFlushInterval The interval at which the event -processor will reset its set of known user keys, in milliseconds. -@tparam[opt] table config.privateAttributeNames Marks a set of user attribute -names private. Any users sent to LaunchDarkly with this configuration active -will have attributes with these names removed. -@param[opt] backend config.featureStoreBackend Persistent feature store -backend. -@tparam int timeoutMilliseconds How long to wait for flags to -download. If the timeout is reached a non fully initialized client will be -returned. -@return A fresh client. -*/ -static int -LuaLDClientInit(lua_State *const l) -{ - struct LDClient *client; - struct LDConfig *config; - unsigned int timeout; - - if (lua_gettop(l) != 2) { - return luaL_error(l, "expecting exactly 2 arguments"); - } - - config = makeConfig(l, 1); - - timeout = luaL_checkinteger(l, 2); - - client = LDClientInit(config, timeout); - - struct LDClient **c = - (struct LDClient **)lua_newuserdata(l, sizeof(client)); - - *c = client; - - luaL_getmetatable(l, "LaunchDarklyClient"); - lua_setmetatable(l, -2); - - return 1; -} - -static int -LuaLDClientClose(lua_State *const l) -{ - struct LDClient **client; - - client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); - - if (*client) { - LDClientClose(*client); - *client = NULL; - } - - return 0; -} - -static void -LuaPushDetails(lua_State *const l, struct LDDetails *const details, - struct LDJSON *const value) -{ - struct LDJSON *reason; - - reason = LDReasonToJSON(details); - - lua_newtable(l); - - LuaPushJSON(l, reason); - lua_setfield(l, -2, "reason"); - - if (details->hasVariation) { - lua_pushnumber(l, details->variationIndex); - lua_setfield(l, -2, "variationIndex"); - } - - LuaPushJSON(l, value); - lua_setfield(l, -2, "value"); - - LDDetailsClear(details); - LDJSONFree(value); - LDJSONFree(reason); -} - -/** -Evaluate a boolean flag -@class function -@name boolVariation -@tparam user user An opaque user object from @{makeUser} -@tparam string key The key of the flag to evaluate. -@tparam boolean fallback The value to return on error -@treturn boolean The evaluation result, or the fallback value -*/ -static int -LuaLDClientBoolVariation(lua_State *const l) -{ - struct LDClient **client; - struct LDUser **user; - - if (lua_gettop(l) != 4) { - return luaL_error(l, "expecting exactly 4 arguments"); - } - - client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); - - user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); - - const char *const key = luaL_checkstring(l, 3); - - const int fallback = lua_toboolean(l, 4); - - const LDBoolean result = - LDBoolVariation(*client, *user, key, fallback, NULL); - - lua_pushboolean(l, result); - - return 1; -} - -/*** -Evaluate a boolean flag and return an explanation -@class function -@name boolVariationDetail -@tparam user user An opaque user object from @{makeUser} -@tparam string key The key of the flag to evaluate. -@tparam boolean fallback The value to return on error -@treturn table The evaluation explanation -*/ -static int -LuaLDClientBoolVariationDetail(lua_State *const l) -{ - struct LDClient **client; - struct LDUser **user; - struct LDDetails details; - - if (lua_gettop(l) != 4) { - return luaL_error(l, "expecting exactly 4 arguments"); - } - - LDDetailsInit(&details); - - client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); - - user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); - - const char *const key = luaL_checkstring(l, 3); - - const int fallback = lua_toboolean(l, 4); - - const LDBoolean result = - LDBoolVariation(*client, *user, key, fallback, &details); - - LuaPushDetails(l, &details, LDNewBool(result)); - - return 1; -} - -/*** -Evaluate an integer flag -@class function -@name intVariation -@tparam user user An opaque user object from @{makeUser} -@tparam string key The key of the flag to evaluate. -@tparam int fallback The value to return on error -@treturn int The evaluation result, or the fallback value -*/ -static int -LuaLDClientIntVariation(lua_State *const l) -{ - struct LDClient **client; - struct LDUser **user; - - if (lua_gettop(l) != 4) { - return luaL_error(l, "expecting exactly 4 arguments"); - } - - client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); - - user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); - - const char *const key = luaL_checkstring(l, 3); - - const int fallback = luaL_checkinteger(l, 4); - - const int result = LDIntVariation(*client, *user, key, fallback, NULL); - - lua_pushnumber(l, result); - - return 1; -} - -/*** -Evaluate an integer flag and return an explanation -@class function -@name intVariationDetail -@tparam user user An opaque user object from @{makeUser} -@tparam string key The key of the flag to evaluate. -@tparam int fallback The value to return on error -@treturn table The evaluation explanation -*/ -static int -LuaLDClientIntVariationDetail(lua_State *const l) -{ - struct LDClient **client; - struct LDUser **user; - struct LDDetails details; - - if (lua_gettop(l) != 4) { - return luaL_error(l, "expecting exactly 4 arguments"); - } - - LDDetailsInit(&details); - - client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); - - user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); - - const char *const key = luaL_checkstring(l, 3); - - const int fallback = luaL_checkinteger(l, 4); - - const int result = LDIntVariation(*client, *user, key, fallback, &details); - - LuaPushDetails(l, &details, LDNewNumber(result)); - - return 1; -} - -/*** -Evaluate a double flag -@class function -@name doubleVariation -@tparam user user An opaque user object from @{makeUser} -@tparam string key The key of the flag to evaluate. -@tparam number fallback The value to return on error -@treturn double The evaluation result, or the fallback value -*/ -static int -LuaLDClientDoubleVariation(lua_State *const l) -{ - struct LDClient **client; - struct LDUser **user; - - if (lua_gettop(l) != 4) { - return luaL_error(l, "expecting exactly 4 arguments"); - } - - client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); - - user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); - - const char *const key = luaL_checkstring(l, 3); - - const double fallback = lua_tonumber(l, 4); - - const double result = - LDDoubleVariation(*client, *user, key, fallback, NULL); - - lua_pushnumber(l, result); - - return 1; -} - -/*** -Evaluate a double flag and return an explanation -@class function -@name doubleVariationDetail -@tparam user user An opaque user object from @{makeUser} -@tparam string key The key of the flag to evaluate. -@tparam number fallback The value to return on error -@treturn table The evaluation explanation -*/ -static int -LuaLDClientDoubleVariationDetail(lua_State *const l) -{ - struct LDClient **client; - struct LDUser **user; - struct LDDetails details; - - if (lua_gettop(l) != 4) { - return luaL_error(l, "expecting exactly 4 arguments"); - } - - LDDetailsInit(&details); - - client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); - - user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); - - const char *const key = luaL_checkstring(l, 3); - - const double fallback = lua_tonumber(l, 4); - - const double result = - LDDoubleVariation(*client, *user, key, fallback, &details); - - LuaPushDetails(l, &details, LDNewNumber(result)); - - return 1; -} - -/*** -Evaluate a string flag -@class function -@name stringVariation -@tparam user user An opaque user object from @{makeUser} -@tparam string key The key of the flag to evaluate. -@tparam string fallback The value to return on error -@treturn string The evaluation result, or the fallback value -*/ -static int -LuaLDClientStringVariation(lua_State *const l) -{ - struct LDClient **client; - struct LDUser **user; - - if (lua_gettop(l) != 4) { - return luaL_error(l, "expecting exactly 4 arguments"); - } - - client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); - - user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); - - const char *const key = luaL_checkstring(l, 3); - - const char *const fallback = luaL_checkstring(l, 4); - - char *const result = - LDStringVariation(*client, *user, key, fallback, NULL); - - lua_pushstring(l, result); - - LDFree(result); - - return 1; -} - -/*** -Evaluate a string flag and return an explanation -@class function -@name stringVariationDetail -@tparam user user An opaque user object from @{makeUser} -@tparam string key The key of the flag to evaluate. -@tparam string fallback The value to return on error -@treturn table The evaluation explanation -*/ -static int -LuaLDClientStringVariationDetail(lua_State *const l) -{ - struct LDClient **client; - struct LDUser **user; - struct LDDetails details; - - if (lua_gettop(l) != 4) { - return luaL_error(l, "expecting exactly 4 arguments"); - } - - LDDetailsInit(&details); - - client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); - - user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); - - const char *const key = luaL_checkstring(l, 3); - - const char *const fallback = luaL_checkstring(l, 4); - - char *const result = - LDStringVariation(*client, *user, key, fallback, &details); - - LuaPushDetails(l, &details, LDNewText(result)); - - LDFree(result); - - return 1; -} - -/*** -Evaluate a json flag -@class function -@name jsonVariation -@tparam user user An opaque user object from @{makeUser} -@tparam string key The key of the flag to evaluate. -@tparam table fallback The value to return on error -@treturn table The evaluation result, or the fallback value -*/ -static int -LuaLDClientJSONVariation(lua_State *const l) -{ - struct LDClient **client; - struct LDUser **user; - struct LDJSON *fallback, *result; - - if (lua_gettop(l) != 4) { - return luaL_error(l, "expecting exactly 4 arguments"); - } - - client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); - - user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); - - const char *const key = luaL_checkstring(l, 3); - - fallback = LuaValueToJSON(l, 4); - - result = LDJSONVariation(*client, *user, key, fallback, NULL); - - LuaPushJSON(l, result); - - LDJSONFree(fallback); - LDJSONFree(result); - - return 1; -} - -/*** -Evaluate a json flag and return an explanation -@class function -@name jsonVariationDetail -@tparam user user An opaque user object from @{makeUser} -@tparam string key The key of the flag to evaluate. -@tparam table fallback The value to return on error -@treturn table The evaluation explanation -*/ -static int -LuaLDClientJSONVariationDetail(lua_State *const l) -{ - struct LDClient **client; - struct LDUser **user; - struct LDJSON *fallback, *result; - struct LDDetails details; - - if (lua_gettop(l) != 4) { - return luaL_error(l, "expecting exactly 4 arguments"); - } - - LDDetailsInit(&details); - - client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); - - user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); - - const char *const key = luaL_checkstring(l, 3); - - fallback = LuaValueToJSON(l, 4); - - result = LDJSONVariation(*client, *user, key, fallback, &details); - - LuaPushDetails(l, &details, result); - - LDJSONFree(fallback); - - return 1; -} - -/*** -Immediately flushes queued events. -@function flush -@treturn nil -*/ -static int -LuaLDClientFlush(lua_State *const l) -{ - struct LDClient **client; - - if (lua_gettop(l) != 1) { - return luaL_error(l, "expecting exactly 1 argument"); - } - - client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); - - LDClientFlush(*client); - - return 0; -} - -/*** -Reports that a user has performed an event. Custom data, and a metric -can be attached to the event as JSON. -@function track -@tparam string key The name of the event -@tparam user user An opaque user object from @{makeUser} -@tparam[opt] table data A value to be associated with an event -@tparam[optchain] number metric A value to be associated with an event -@treturn nil -*/ -static int -LuaLDClientTrack(lua_State *const l) -{ - struct LDClient **client; - struct LDUser **user; - struct LDJSON *value; - - if (lua_gettop(l) < 3 || lua_gettop(l) > 5) { - return luaL_error(l, "expecting 3-5 arguments"); - } - - client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); - - const char *const key = luaL_checkstring(l, 2); - - user = (struct LDUser **)luaL_checkudata(l, 3, "LaunchDarklyUser"); - - if (lua_isnil(l, 4)) { - value = NULL; - } else { - value = LuaValueToJSON(l, 4); - } - - if (lua_gettop(l) == 5 && lua_isnumber(l, 5)) { - const double metric = luaL_checknumber(l, 5); - - LDClientTrackMetric(*client, key, *user, value, metric); - } else { - LDClientTrack(*client, key, *user, value); - } - - return 0; -} - -/*** -Associates two users for analytics purposes by generating an alias event. -@function alias -@tparam user currentUser An opaque user object from @{makeUser} -@tparam user previousUser An opaque user object from @{makeUser} -@treturn nil -*/ -static int -LuaLDClientAlias(lua_State *const l) -{ - struct LDClient **client; - struct LDUser **currentUser, **previousUser; - struct LDJSON *value; - - if (lua_gettop(l) != 3) { - return luaL_error(l, "expected exactly three arguments"); - } - - client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); - currentUser = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); - previousUser = (struct LDUser **)luaL_checkudata(l, 3, "LaunchDarklyUser"); - - if (!LDClientAlias(*client, *currentUser, *previousUser)) { - return luaL_error(l, "LDClientAlias failed"); - } - - return 0; -} - -/*** -Check if a client has been fully initialized. This may be useful if the -initialization timeout was reached. -@function isInitialized -@treturn boolean true if fully initialized -*/ -static int -LuaLDClientIsInitialized(lua_State *const l) -{ - struct LDClient **client; - - if (lua_gettop(l) != 1) { - return luaL_error(l, "expecting exactly 1 argument"); - } - - client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); - - lua_pushboolean(l, LDClientIsInitialized(*client)); - - return 1; -} - -/*** -Generates an identify event for a user. -@function identify -@tparam user user An opaque user object from @{makeUser} -@treturn nil -*/ -static int -LuaLDClientIdentify(lua_State *const l) -{ - struct LDClient **client; - struct LDUser **user; - - if (lua_gettop(l) != 2) { - return luaL_error(l, "expecting exactly 2 arguments"); - } - - client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); - user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); - - LDClientIdentify(*client, *user); - - return 0; -} - -/*** -Returns a map from feature flag keys to values for a given user. -This does not send analytics events back to LaunchDarkly. -@function allFlags -@tparam user user An opaque user object from @{makeUser} -@treturn table -*/ -static int -LuaLDClientAllFlags(lua_State *const l) -{ - struct LDClient **client; - struct LDUser **user; - struct LDJSON *result; - - if (lua_gettop(l) != 2) { - return luaL_error(l, "expecting exactly 2 arguments"); - } - - client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); - - user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); - - result = LDAllFlags(*client, *user); - - LuaPushJSON(l, result); - - LDJSONFree(result); - - return 1; -} - -static const struct luaL_Reg launchdarkly_functions[] = { - { "clientInit", LuaLDClientInit }, - { "makeUser", LuaLDUserNew }, - { "registerLogger", LuaLDRegisterLogger }, - { "version", LuaLDVersion }, - { NULL, NULL } -}; - -static const struct luaL_Reg launchdarkly_client_methods[] = { - { "boolVariation", LuaLDClientBoolVariation }, - { "boolVariationDetail", LuaLDClientBoolVariationDetail }, - { "intVariation", LuaLDClientIntVariation }, - { "intVariationDetail", LuaLDClientIntVariationDetail }, - { "doubleVariation", LuaLDClientDoubleVariation }, - { "doubleVariationDetail", LuaLDClientDoubleVariationDetail }, - { "stringVariation", LuaLDClientStringVariation }, - { "stringVariationDetail", LuaLDClientStringVariationDetail }, - { "jsonVariation", LuaLDClientJSONVariation }, - { "jsonVariationDetail", LuaLDClientJSONVariationDetail }, - { "flush", LuaLDClientFlush }, - { "track", LuaLDClientTrack }, - { "alias", LuaLDClientAlias }, - { "allFlags", LuaLDClientAllFlags }, - { "isInitialized", LuaLDClientIsInitialized }, - { "identify", LuaLDClientIdentify }, - { "__gc", LuaLDClientClose }, - { NULL, NULL } -}; - -static const struct luaL_Reg launchdarkly_user_methods[] = { - { "__gc", LuaLDUserFree }, - { NULL, NULL } -}; - -static const struct luaL_Reg launchdarkly_store_methods[] = { - { NULL, NULL } -}; - -/* -** Adapted from Lua 5.2.0 -*/ -static void -ld_luaL_setfuncs(lua_State *L, const luaL_Reg *l, int nup) -{ - luaL_checkstack(L, nup+1, "too many upvalues"); - for (; l->name != NULL; l++) { /* fill the table with given functions */ - int i; - lua_pushstring(L, l->name); - /* copy upvalues to the top */ - for (i = 0; i < nup; i++) { - lua_pushvalue(L, -(nup+1)); - } - lua_pushcclosure(L, l->func, nup); /* closure with those upvalues */ - lua_settable(L, -(nup + 3)); - } - lua_pop(L, nup); /* remove upvalues */ -} - -int -luaopen_launchdarkly_server_sdk(lua_State *const l) -{ - luaL_newmetatable(l, "LaunchDarklyClient"); - lua_pushvalue(l, -1); - lua_setfield(l, -2, "__index"); - ld_luaL_setfuncs(l, launchdarkly_client_methods, 0); - - luaL_newmetatable(l, "LaunchDarklyUser"); - lua_pushvalue(l, -1); - lua_setfield(l, -2, "__index"); - ld_luaL_setfuncs(l, launchdarkly_user_methods, 0); - - luaL_newmetatable(l, "LaunchDarklyStoreInterface"); - lua_pushvalue(l, -1); - lua_setfield(l, -2, "__index"); - ld_luaL_setfuncs(l, launchdarkly_store_methods, 0); - - #if LUA_VERSION_NUM == 503 || LUA_VERSION_NUM == 502 - luaL_newlib(l, launchdarkly_functions); - #else - luaL_register(l, "launchdarkly-server-sdk", launchdarkly_functions); - #endif - - return 1; -} From 8dcaac698e0123f37421c9b4e632e804b2de7f17 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 4 May 2022 15:06:18 -0700 Subject: [PATCH 040/161] Update master->main in pull request template (#37) --- .github/pull_request_template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 1980676..fc89ce0 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,7 +1,7 @@ **Requirements** - [ ] I have added test coverage for new or changed functionality -- [ ] I have followed the repository's [pull request submission guidelines](../blob/master/CONTRIBUTING.md#submitting-pull-requests) +- [ ] I have followed the repository's [pull request submission guidelines](../blob/main/CONTRIBUTING.md#submitting-pull-requests) - [ ] I have validated my changes against all supported platform versions **Related issues** From f823656637fc0997102478ff7ec16fb976732ff9 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 31 Aug 2022 15:15:05 -0700 Subject: [PATCH 041/161] Fix CircleCI build, update to matrix builds for Ubuntu 18.04 and 20.04 (#38) * Refactor lua 5.2 & 5.3 jobs as a circleCI matrix job * Test Ubuntu 20.04 & 18.04 --- .circleci/config.yml | 108 +++++++++++++++++------------------ .ldrelease/build-c-server.sh | 2 +- 2 files changed, 53 insertions(+), 57 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index c5b17d8..6f8e5dc 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,94 +4,90 @@ workflows: version: 2 all: jobs: - - build-test-linux-luajit - - build-test-linux-lua53 - - build-test-linux-lua52 + - build-test-linux-luajit: + matrix: + parameters: + ubuntu-version: ["18.04", "20.04"] + - build-test-linux-lua: + matrix: + parameters: + lua-version: ["5.2", "5.3"] + ubuntu-version: ["18.04", "20.04"] -jobs: - build-test-linux-luajit: - docker: - - image: ubuntu:18.04 - - image: redis + +commands: + # Fetches a recent version of cmake from Kitware repositories. + # This is necessary for building the C Server SDK. + get-cmake: steps: - - checkout - run: - name: Prepare + name: Install recent cmake command: | - apt-get update -y - apt-get install -y luajit libluajit-5.1-dev lua-ldoc zip \ - ca-certificates curl libpcre3 libcurl4-openssl-dev \ - cmake libhiredis-dev git build-essential libpcre3-dev luarocks + codename=$(cat /etc/os-release | grep UBUNTU_CODENAME | sed -e "s/^UBUNTU_CODENAME=//") + wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | sudo tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null + echo "deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ ${codename} main" | sudo tee /etc/apt/sources.list.d/kitware.list >/dev/null + sudo apt purge --auto-remove cmake + sudo apt-get update -y + sudo apt-get install -y cmake + # Builds the C Server SDK, followed by the Lua Server SDK. + build-sdk: + steps: - run: name: Build c-server-sdk command: ./.ldrelease/build-c-server.sh - - run: name: Build lua-server-sdk command: | - luarocks make launchdarkly-server-sdk-1.0-0.rockspec - luarocks make launchdarkly-server-sdk-redis-1.0-0.rockspec + sudo luarocks make launchdarkly-server-sdk-1.0-0.rockspec + sudo luarocks make launchdarkly-server-sdk-redis-1.0-0.rockspec - - run: - name: Run tests - command: | - luajit test.lua - build-test-linux-lua53: +jobs: + build-test-linux-luajit: + parameters: + ubuntu-version: + type: string docker: - - image: ubuntu:18.04 + - image: cimg/base:current-<< parameters.ubuntu-version >> - image: redis steps: - checkout - run: name: Prepare command: | - apt-get update -y - apt-get install -y lua5.3 liblua5.3-dev lua-ldoc zip \ + sudo apt-get update -y + sudo apt-get install -y luajit libluajit-5.1-dev lua-ldoc zip \ ca-certificates curl libpcre3 libcurl4-openssl-dev \ - cmake libhiredis-dev git build-essential libpcre3-dev luarocks - - - run: - name: Build c-server-sdk - command: ./.ldrelease/build-c-server.sh - - - run: - name: Build lua-server-sdk - command: | - luarocks make launchdarkly-server-sdk-1.0-0.rockspec - luarocks make launchdarkly-server-sdk-redis-1.0-0.rockspec - + libhiredis-dev git build-essential libpcre3-dev luarocks wget + - get-cmake + - build-sdk - run: name: Run tests command: | - lua5.3 test.lua + luajit test.lua - build-test-linux-lua52: + build-test-linux-lua: + parameters: + lua-version: + type: string + ubuntu-version: + type: string docker: - - image: ubuntu:18.04 + - image: cimg/base:current-<< parameters.ubuntu-version >> - image: redis steps: - checkout - run: - name: Prepare + name: Install system packages command: | - apt-get update -y - apt-get install -y lua5.2 liblua5.2-dev lua-ldoc zip \ + sudo apt-get update -y + sudo apt-get install -y lua<< parameters.lua-version >> liblua<< parameters.lua-version >>-dev lua-ldoc zip \ ca-certificates curl libpcre3 libcurl4-openssl-dev \ - cmake libhiredis-dev git build-essential libpcre3-dev luarocks - - - run: - name: Build c-server-sdk - command: ./.ldrelease/build-c-server.sh - - - run: - name: Build lua-server-sdk - command: | - luarocks make launchdarkly-server-sdk-1.0-0.rockspec - luarocks make launchdarkly-server-sdk-redis-1.0-0.rockspec - + libhiredis-dev git build-essential libpcre3-dev luarocks wget + - get-cmake + - build-sdk - run: name: Run tests command: | - lua5.2 test.lua + lua<< parameters.lua-version >> test.lua diff --git a/.ldrelease/build-c-server.sh b/.ldrelease/build-c-server.sh index ffa9db3..a5abfa7 100755 --- a/.ldrelease/build-c-server.sh +++ b/.ldrelease/build-c-server.sh @@ -8,4 +8,4 @@ mkdir build cd build cmake -D BUILD_SHARED_LIBS=ON -D BUILD_TESTING=OFF -D REDIS_STORE=ON .. make -make install +sudo make install From 55c30837fb5fdc6a77c01e4d77d46f174292ba5b Mon Sep 17 00:00:00 2001 From: Ember Stevens Date: Wed, 7 Jun 2023 21:49:12 -0700 Subject: [PATCH 042/161] Updates daily flag count --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3f01ae2..19febd1 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ LaunchDarkly Server-Side SDK for Lua LaunchDarkly overview ------------------------- -[LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves over 100 billion feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/home/getting-started) using LaunchDarkly today! +[LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves trillions of feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/home/getting-started) using LaunchDarkly today! [![Twitter Follow](https://img.shields.io/twitter/follow/launchdarkly.svg?style=social&label=Follow&maxAge=2592000)](https://twitter.com/intent/follow?screen_name=launchdarkly) @@ -28,7 +28,7 @@ Refer to the [SDK documentation](https://docs.launchdarkly.com/sdk/server-side/l Learn more ----------- -Check out our [documentation](https://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. You can also head straight to the [complete reference guide for this SDK](https://docs.launchdarkly.com/sdk/server-side/lua). +Read our [documentation](https://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. You can also head straight to the [complete reference guide for this SDK](https://docs.launchdarkly.com/sdk/server-side/lua). Testing ------- From 94a8489d0d986a95c03feaec8b67106c3d0dcdb3 Mon Sep 17 00:00:00 2001 From: "ld-repository-standards[bot]" <113625520+ld-repository-standards[bot]@users.noreply.github.com> Date: Mon, 26 Jun 2023 22:46:13 +0000 Subject: [PATCH 043/161] Add file CODEOWNERS --- CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 CODEOWNERS diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..7d0dac3 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,2 @@ +# Repository Maintainers +* @launchdarkly/team-sdk From 39b8a47ea63d3a1c5c09a2c41c145c2e91410c17 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 5 Oct 2023 13:55:06 -0700 Subject: [PATCH 044/161] chore: update CODEOWNERS (#41) Team `c` because this is a C binding and probably doesn't make sense to have a Lua team. --- CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 7d0dac3..412f35e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,2 +1,2 @@ # Repository Maintainers -* @launchdarkly/team-sdk +* @launchdarkly/team-sdk-c From c459931fd8a9b8563593ee275fd642bce232cba9 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 11 Dec 2023 12:39:36 -0800 Subject: [PATCH 045/161] rename .ldrelease dir to scripts --- {.ldrelease => scripts}/build-c-server.sh | 0 {.ldrelease => scripts}/build-docs.sh | 0 {.ldrelease => scripts}/config.yml | 0 {.ldrelease => scripts}/prepare.sh | 0 {.ldrelease => scripts}/update-version.sh | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename {.ldrelease => scripts}/build-c-server.sh (100%) rename {.ldrelease => scripts}/build-docs.sh (100%) rename {.ldrelease => scripts}/config.yml (100%) rename {.ldrelease => scripts}/prepare.sh (100%) rename {.ldrelease => scripts}/update-version.sh (100%) diff --git a/.ldrelease/build-c-server.sh b/scripts/build-c-server.sh similarity index 100% rename from .ldrelease/build-c-server.sh rename to scripts/build-c-server.sh diff --git a/.ldrelease/build-docs.sh b/scripts/build-docs.sh similarity index 100% rename from .ldrelease/build-docs.sh rename to scripts/build-docs.sh diff --git a/.ldrelease/config.yml b/scripts/config.yml similarity index 100% rename from .ldrelease/config.yml rename to scripts/config.yml diff --git a/.ldrelease/prepare.sh b/scripts/prepare.sh similarity index 100% rename from .ldrelease/prepare.sh rename to scripts/prepare.sh diff --git a/.ldrelease/update-version.sh b/scripts/update-version.sh similarity index 100% rename from .ldrelease/update-version.sh rename to scripts/update-version.sh From 7282b70be0e5113ad4c534b9571525e3558d6834 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 11 Dec 2023 12:39:48 -0800 Subject: [PATCH 046/161] add build docs action --- .github/actions/build-docs/action.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .github/actions/build-docs/action.yml diff --git a/.github/actions/build-docs/action.yml b/.github/actions/build-docs/action.yml new file mode 100644 index 0000000..df4c384 --- /dev/null +++ b/.github/actions/build-docs/action.yml @@ -0,0 +1,9 @@ +name: Build Documentation +description: 'Build Documentation.' + +runs: + using: composite + steps: + - name: Build Documentation + shell: bash + run: ./scripts/build-docs.sh From a57331a8b568953e5808dbfb2d8c5f7a66f87428 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 11 Dec 2023 13:01:21 -0800 Subject: [PATCH 047/161] ci: add github action to replace CircleCI --- .github/actions/ci/action.yml | 25 +++++++++++++++++++++++++ .github/workflows/ci.yml | 27 +++++++++++++++++++++++++++ .github/workflows/lint-pr-title.yml | 12 ++++++++++++ 3 files changed, 64 insertions(+) create mode 100644 .github/actions/ci/action.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/lint-pr-title.yml diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml new file mode 100644 index 0000000..647f500 --- /dev/null +++ b/.github/actions/ci/action.yml @@ -0,0 +1,25 @@ +name: CI Workflow +description: 'Shared CI workflow.' +inputs: + lua-version: + description: 'Which version of Lua to use?' + required: false + default: "5.3" + + +runs: + using: composite + steps: + - name: Setup Lua + uses: leafo/gh-actions-lua@v10.0.0 + with: + luaVersion: ${{ inputs.lua-version }} + - name: Setup C Server-side SDK + shell: bash + run: ./scripts/build-c-server.sh + - name: Build Lua Server-side SDK + shell: bash + run: sudo luarocks make launchdarkly-server-sdk-1.0-0.rockspec + - name: Build Lua Server-side SDK with Redis + shell: bash + run: sudo luarocks make launchdarkly-server-sdk-redis-1.0-0.rockspec diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0437e17 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: Run CI +on: + push: + branches: [ main ] + paths-ignore: + - '**.md' # Do not need to run CI for markdown changes. + pull_request: + branches: [ main ] + paths-ignore: + - '**.md' + +jobs: + linux-build: + runs-on: ubuntu-latest + + strategy: + matrix: + lua-versions: [5.2, 5.3, luajit-2.0.5] + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # If you only need the current version keep this. + + - uses: ./.github/actions/ci + with: + lua-version: ${{ matrix.lua-version }} diff --git a/.github/workflows/lint-pr-title.yml b/.github/workflows/lint-pr-title.yml new file mode 100644 index 0000000..4ba79c1 --- /dev/null +++ b/.github/workflows/lint-pr-title.yml @@ -0,0 +1,12 @@ +name: Lint PR title + +on: + pull_request_target: + types: + - opened + - edited + - synchronize + +jobs: + lint-pr-title: + uses: launchdarkly/gh-actions/.github/workflows/lint-pr-title.yml@main From eb986dee0fbd311b3581a1f50f3594bfe4f1457b Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 11 Dec 2023 13:29:43 -0800 Subject: [PATCH 048/161] force lua 5.3 temporarily --- .github/actions/ci/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index 647f500..baf6789 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -13,7 +13,7 @@ runs: - name: Setup Lua uses: leafo/gh-actions-lua@v10.0.0 with: - luaVersion: ${{ inputs.lua-version }} + luaVersion: "5.3" - name: Setup C Server-side SDK shell: bash run: ./scripts/build-c-server.sh From 42f079df8a4fe85cc613e147bb917117efbbc88b Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 11 Dec 2023 13:32:15 -0800 Subject: [PATCH 049/161] rename matrix version --- .github/actions/ci/action.yml | 6 ++---- .github/workflows/ci.yml | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index baf6789..db217e6 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -3,9 +3,7 @@ description: 'Shared CI workflow.' inputs: lua-version: description: 'Which version of Lua to use?' - required: false - default: "5.3" - + required: true runs: using: composite @@ -13,7 +11,7 @@ runs: - name: Setup Lua uses: leafo/gh-actions-lua@v10.0.0 with: - luaVersion: "5.3" + luaVersion: ${{ inputs.lua-version }} - name: Setup C Server-side SDK shell: bash run: ./scripts/build-c-server.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0437e17..a7149b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: - lua-versions: [5.2, 5.3, luajit-2.0.5] + version: ["5.2", "5.3", "luajit-2.0.5"] steps: - uses: actions/checkout@v4 @@ -24,4 +24,4 @@ jobs: - uses: ./.github/actions/ci with: - lua-version: ${{ matrix.lua-version }} + lua-version: ${{ matrix.version }} From b7b70570754f65788e3f4e2656f10e90355802f0 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 11 Dec 2023 13:39:41 -0800 Subject: [PATCH 050/161] add libcurl --- .github/actions/ci/action.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index db217e6..1570143 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -12,6 +12,11 @@ runs: uses: leafo/gh-actions-lua@v10.0.0 with: luaVersion: ${{ inputs.lua-version }} + - name: Install C Server-side SDK Dependencies + shell: bash + run: | + sudo apt-get update -y + sudo apt-get install -y libcurl4-openssl-dev - name: Setup C Server-side SDK shell: bash run: ./scripts/build-c-server.sh From 5f330b5814ddd3cef3c156e7741c38440c87c0c3 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 11 Dec 2023 13:41:31 -0800 Subject: [PATCH 051/161] add redis --- .github/actions/ci/action.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index 1570143..b46d22e 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -17,6 +17,8 @@ runs: run: | sudo apt-get update -y sudo apt-get install -y libcurl4-openssl-dev + sudo apt-get install -y libhiredis-dev + - name: Setup C Server-side SDK shell: bash run: ./scripts/build-c-server.sh From d07c4e830c37e10969d5945030ae9fccf1d67ab2 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 11 Dec 2023 13:45:21 -0800 Subject: [PATCH 052/161] Add luarocks --- .github/actions/ci/action.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index b46d22e..120251a 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -4,14 +4,20 @@ inputs: lua-version: description: 'Which version of Lua to use?' required: true + luarocks-version: + description: 'Which version of LuaRocks to use?' + required: false + default: "3.1.3" runs: using: composite steps: - name: Setup Lua uses: leafo/gh-actions-lua@v10.0.0 + uses: leafo/gh-actions-luarocks@v4.3.0 with: luaVersion: ${{ inputs.lua-version }} + luaRocksVersion: ${{ inputs.luarocks-version }} - name: Install C Server-side SDK Dependencies shell: bash run: | From 30e57774fc75b41f5052b41f4e3f952ec21a2e83 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 11 Dec 2023 13:54:06 -0800 Subject: [PATCH 053/161] fix uses syntax --- .github/actions/ci/action.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index 120251a..f87af3c 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -12,9 +12,8 @@ inputs: runs: using: composite steps: - - name: Setup Lua - uses: leafo/gh-actions-lua@v10.0.0 - uses: leafo/gh-actions-luarocks@v4.3.0 + - uses: leafo/gh-actions-lua@v10.0.0 + - uses: leafo/gh-actions-luarocks@v4.3.0 with: luaVersion: ${{ inputs.lua-version }} luaRocksVersion: ${{ inputs.luarocks-version }} From 0c47b6607759d9750e0e87c8bd92291541c9a75c Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 11 Dec 2023 14:02:58 -0800 Subject: [PATCH 054/161] remove sudo --- .github/actions/ci/action.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index f87af3c..455cfa7 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -4,10 +4,6 @@ inputs: lua-version: description: 'Which version of Lua to use?' required: true - luarocks-version: - description: 'Which version of LuaRocks to use?' - required: false - default: "3.1.3" runs: using: composite @@ -16,7 +12,6 @@ runs: - uses: leafo/gh-actions-luarocks@v4.3.0 with: luaVersion: ${{ inputs.lua-version }} - luaRocksVersion: ${{ inputs.luarocks-version }} - name: Install C Server-side SDK Dependencies shell: bash run: | @@ -29,7 +24,7 @@ runs: run: ./scripts/build-c-server.sh - name: Build Lua Server-side SDK shell: bash - run: sudo luarocks make launchdarkly-server-sdk-1.0-0.rockspec + run: luarocks make launchdarkly-server-sdk-1.0-0.rockspec - name: Build Lua Server-side SDK with Redis shell: bash - run: sudo luarocks make launchdarkly-server-sdk-redis-1.0-0.rockspec + run: luarocks make launchdarkly-server-sdk-redis-1.0-0.rockspec From 544b5a16f549b5c6e4c78c8a6acb15b10a9e659a Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 11 Dec 2023 14:09:23 -0800 Subject: [PATCH 055/161] run unit tests --- .github/actions/ci/action.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index 455cfa7..051cf49 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -22,9 +22,15 @@ runs: - name: Setup C Server-side SDK shell: bash run: ./scripts/build-c-server.sh + - name: Build Lua Server-side SDK shell: bash run: luarocks make launchdarkly-server-sdk-1.0-0.rockspec + - name: Build Lua Server-side SDK with Redis shell: bash run: luarocks make launchdarkly-server-sdk-redis-1.0-0.rockspec + + - name: Run tests + shell: bash + run: lua test.lua From 020766b32a7c0156969843b12025efea0deac919 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 11 Dec 2023 14:15:18 -0800 Subject: [PATCH 056/161] remove luaL_register call --- launchdarkly-server-sdk-redis.c | 8 +------- launchdarkly-server-sdk.c | 6 +----- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/launchdarkly-server-sdk-redis.c b/launchdarkly-server-sdk-redis.c index 606b63b..ac04bc8 100644 --- a/launchdarkly-server-sdk-redis.c +++ b/launchdarkly-server-sdk-redis.c @@ -99,12 +99,6 @@ static void luaL_setfuncs (lua_State *L, const luaL_Reg *l, int nup) { int luaopen_launchdarkly_server_sdk_redis(lua_State *const l) { - #if LUA_VERSION_NUM == 503 || LUA_VERSION_NUM == 502 - luaL_newlib(l, launchdarkly_functions); - #else - luaL_register(l, "launchdarkly-server-sdk-redis", - launchdarkly_functions); - #endif - + luaL_newlib(l, launchdarkly_functions); return 1; } diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 725762b..e10ec51 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -1256,11 +1256,7 @@ luaopen_launchdarkly_server_sdk(lua_State *const l) lua_setfield(l, -2, "__index"); ld_luaL_setfuncs(l, launchdarkly_store_methods, 0); - #if LUA_VERSION_NUM == 503 || LUA_VERSION_NUM == 502 - luaL_newlib(l, launchdarkly_functions); - #else - luaL_register(l, "launchdarkly-server-sdk", launchdarkly_functions); - #endif + luaL_newlib(l, launchdarkly_functions); return 1; } From 78bf8c2aa72bb12867b6901cd4dedfe2fe1ad4be Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 11 Dec 2023 14:21:00 -0800 Subject: [PATCH 057/161] use luajit test --- .github/actions/ci/action.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index 051cf49..b2a0d57 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -9,9 +9,9 @@ runs: using: composite steps: - uses: leafo/gh-actions-lua@v10.0.0 - - uses: leafo/gh-actions-luarocks@v4.3.0 with: luaVersion: ${{ inputs.lua-version }} + - uses: leafo/gh-actions-luarocks@v4.3.0 - name: Install C Server-side SDK Dependencies shell: bash run: | @@ -33,4 +33,10 @@ runs: - name: Run tests shell: bash + if: ${{ ! contains(inputs.lua-version, 'jit') }} run: lua test.lua + + - name: Run tests (JIT) + shell: bash + if: ${{ contains(inputs.lua-version, 'jit') }} + run: luajit test.lua From 45962d4131b1f3dfd94939e636648f538e16232f Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 11 Dec 2023 14:34:38 -0800 Subject: [PATCH 058/161] add publish docs action --- .github/actions/build-docs/action.yml | 2 +- .github/actions/publish-docs/action.yml | 15 +++++++++++++++ .github/workflows/ci.yml | 2 +- scripts/build-docs.sh | 10 +++++++--- 4 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 .github/actions/publish-docs/action.yml diff --git a/.github/actions/build-docs/action.yml b/.github/actions/build-docs/action.yml index df4c384..93953bb 100644 --- a/.github/actions/build-docs/action.yml +++ b/.github/actions/build-docs/action.yml @@ -6,4 +6,4 @@ runs: steps: - name: Build Documentation shell: bash - run: ./scripts/build-docs.sh + run: ./scripts/build-docs.sh docs diff --git a/.github/actions/publish-docs/action.yml b/.github/actions/publish-docs/action.yml new file mode 100644 index 0000000..fb5a1c4 --- /dev/null +++ b/.github/actions/publish-docs/action.yml @@ -0,0 +1,15 @@ +name: Publish Documentation +description: 'Publish the documentation to GitHub pages' +inputs: + token: + description: 'Token to use for publishing.' + required: true + +runs: + using: composite + steps: + - uses: launchdarkly/gh-actions/actions/publish-pages@publish-pages-v1.0.1 + name: 'Publish to Github pages' + with: + docs_path: docs + github_token: ${{ inputs.token }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a7149b5..3c272cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: - version: ["5.2", "5.3", "luajit-2.0.5"] + version: ["5.1", "5.2", "5.3", "luajit-2.0.5"] steps: - uses: actions/checkout@v4 diff --git a/scripts/build-docs.sh b/scripts/build-docs.sh index bbafab2..c23c119 100755 --- a/scripts/build-docs.sh +++ b/scripts/build-docs.sh @@ -2,10 +2,14 @@ set -e -# This only runs in the Linux build, since the docs are the same for all platforms. +# The first argument is the directory where the docs should be built. -# LD_RELEASE_TEMP_DIR is guaranteed to be empty, and will not be checked into version control. -DOCS_BUILD_DIR="$LD_RELEASE_TEMP_DIR/docs" +if [ -z "$1" ]; then + echo "Usage: $0 " + exit 1 +fi + +DOCS_BUILD_DIR=$1 mkdir "$DOCS_BUILD_DIR" cp launchdarkly-server-sdk.c "$DOCS_BUILD_DIR" From 2ce3c0a550d93d5eb5d4058789b10af405b7ebc3 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 11 Dec 2023 14:34:40 -0800 Subject: [PATCH 059/161] Revert "remove luaL_register call" This reverts commit 020766b32a7c0156969843b12025efea0deac919. --- launchdarkly-server-sdk-redis.c | 8 +++++++- launchdarkly-server-sdk.c | 6 +++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/launchdarkly-server-sdk-redis.c b/launchdarkly-server-sdk-redis.c index ac04bc8..606b63b 100644 --- a/launchdarkly-server-sdk-redis.c +++ b/launchdarkly-server-sdk-redis.c @@ -99,6 +99,12 @@ static void luaL_setfuncs (lua_State *L, const luaL_Reg *l, int nup) { int luaopen_launchdarkly_server_sdk_redis(lua_State *const l) { - luaL_newlib(l, launchdarkly_functions); + #if LUA_VERSION_NUM == 503 || LUA_VERSION_NUM == 502 + luaL_newlib(l, launchdarkly_functions); + #else + luaL_register(l, "launchdarkly-server-sdk-redis", + launchdarkly_functions); + #endif + return 1; } diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index e10ec51..725762b 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -1256,7 +1256,11 @@ luaopen_launchdarkly_server_sdk(lua_State *const l) lua_setfield(l, -2, "__index"); ld_luaL_setfuncs(l, launchdarkly_store_methods, 0); - luaL_newlib(l, launchdarkly_functions); + #if LUA_VERSION_NUM == 503 || LUA_VERSION_NUM == 502 + luaL_newlib(l, launchdarkly_functions); + #else + luaL_register(l, "launchdarkly-server-sdk", launchdarkly_functions); + #endif return 1; } From 027be6de381f34049e00f02164a6e29f983b5bbd Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 11 Dec 2023 14:39:08 -0800 Subject: [PATCH 060/161] add manual publish docs --- .github/workflows/manual-publish-docs.yml | 25 +++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/manual-publish-docs.yml diff --git a/.github/workflows/manual-publish-docs.yml b/.github/workflows/manual-publish-docs.yml new file mode 100644 index 0000000..1c3b2c9 --- /dev/null +++ b/.github/workflows/manual-publish-docs.yml @@ -0,0 +1,25 @@ +on: + workflow_dispatch: + +name: Publish Documentation +jobs: + build-publish: + runs-on: ubuntu-latest + permissions: + contents: write # Needed to write github pages. + + steps: + - uses: actions/checkout@v4 + + - name: Build and Test + uses: ./.github/actions/ci + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Build documentation + uses: ./.github/actions/build-docs + + - name: Publish Documentation + uses: ./.github/actions/publish-docs + with: + token: ${{secrets.GITHUB_TOKEN}} From 48921ae7b527abaadb61894c5efd5f075e990fb2 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 11 Dec 2023 14:39:38 -0800 Subject: [PATCH 061/161] add stale workflow --- .github/workflows/stale.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .github/workflows/stale.yml diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..14118d4 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,10 @@ +name: 'Close stale issues and PRs' +on: + workflow_dispatch: + schedule: + # Happen once per day at 1:30 AM + - cron: '30 1 * * *' + +jobs: + sdk-close-stale: + uses: launchdarkly/gh-actions/.github/workflows/sdk-stale.yml@main From e7b8bfebd2978f48eb379277234c93d2cb81d477 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 11 Dec 2023 14:49:28 -0800 Subject: [PATCH 062/161] add release please config --- .github/workflows/release-please.yml | 42 ++++++++++++++++++++++++++++ .release-please-manifest.json | 3 ++ launchdarkly-server-sdk.c | 2 +- release-please-config.json | 14 ++++++++++ 4 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/release-please.yml create mode 100644 .release-please-manifest.json create mode 100644 release-please-config.json diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..58fea42 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,42 @@ +name: Run Release Please + +on: + push: + branches: + - main + +jobs: + release-package: + runs-on: ubuntu-latest + + permissions: + contents: write # Contents and pull-requests are for release-please to make releases. + pull-requests: write + + steps: + - uses: google-github-actions/release-please-action@v3 + id: release + with: + command: manifest + token: ${{ secrets.GITHUB_TOKEN }} + default-branch: main + + - uses: actions/checkout@v4 + if: ${{ steps.release.outputs.releases_created }} + with: + fetch-depth: 0 # If you only need the current version keep this. + + - name: Build and Test + if: ${{ steps.release.outputs.releases_created }} + uses: ./.github/actions/ci + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Build documentation + if: ${{ steps.release.outputs.releases_created }} + uses: ./.github/actions/build-docs + + - uses: ./.github/actions/publish-docs + if: ${{ steps.release.outputs.releases_created }} + with: + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..f6a9e15 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "1.2.2" +} diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 725762b..d5442e5 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -11,7 +11,7 @@ Server-side SDK for LaunchDarkly. #include -#define SDKVersion "1.2.2" +#define SDKVersion "1.2.2" // {x-release-please-version} static struct LDJSON * LuaValueToJSON(lua_State *const l, const int i); diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..dc2a6fa --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,14 @@ +{ + "packages": { + ".": { + "release-type": "simple", + "bump-minor-pre-major": true, + "versioning": "default", + "//Delete this 3": "extra-files is only needed if your release is not supported, or you have extra files that need the version number. Delete the entire node if you don't need it.", + "extra-files": [ + "demo_version.xml", + "demo_version.txt" + ] + } + } +} From e8d9392b96ee7785d16e0f50cd8def9daae2517c Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 11 Dec 2023 14:59:49 -0800 Subject: [PATCH 063/161] update copyright on license --- .circleci/config.yml | 93 -------------------------------------------- LICENSE | 2 +- 2 files changed, 1 insertion(+), 94 deletions(-) delete mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 6f8e5dc..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,93 +0,0 @@ -version: 2.1 - -workflows: - version: 2 - all: - jobs: - - build-test-linux-luajit: - matrix: - parameters: - ubuntu-version: ["18.04", "20.04"] - - build-test-linux-lua: - matrix: - parameters: - lua-version: ["5.2", "5.3"] - ubuntu-version: ["18.04", "20.04"] - - -commands: - # Fetches a recent version of cmake from Kitware repositories. - # This is necessary for building the C Server SDK. - get-cmake: - steps: - - run: - name: Install recent cmake - command: | - codename=$(cat /etc/os-release | grep UBUNTU_CODENAME | sed -e "s/^UBUNTU_CODENAME=//") - wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | sudo tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null - echo "deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ ${codename} main" | sudo tee /etc/apt/sources.list.d/kitware.list >/dev/null - sudo apt purge --auto-remove cmake - sudo apt-get update -y - sudo apt-get install -y cmake - - # Builds the C Server SDK, followed by the Lua Server SDK. - build-sdk: - steps: - - run: - name: Build c-server-sdk - command: ./.ldrelease/build-c-server.sh - - run: - name: Build lua-server-sdk - command: | - sudo luarocks make launchdarkly-server-sdk-1.0-0.rockspec - sudo luarocks make launchdarkly-server-sdk-redis-1.0-0.rockspec - - -jobs: - build-test-linux-luajit: - parameters: - ubuntu-version: - type: string - docker: - - image: cimg/base:current-<< parameters.ubuntu-version >> - - image: redis - steps: - - checkout - - run: - name: Prepare - command: | - sudo apt-get update -y - sudo apt-get install -y luajit libluajit-5.1-dev lua-ldoc zip \ - ca-certificates curl libpcre3 libcurl4-openssl-dev \ - libhiredis-dev git build-essential libpcre3-dev luarocks wget - - get-cmake - - build-sdk - - run: - name: Run tests - command: | - luajit test.lua - - build-test-linux-lua: - parameters: - lua-version: - type: string - ubuntu-version: - type: string - docker: - - image: cimg/base:current-<< parameters.ubuntu-version >> - - image: redis - steps: - - checkout - - run: - name: Install system packages - command: | - sudo apt-get update -y - sudo apt-get install -y lua<< parameters.lua-version >> liblua<< parameters.lua-version >>-dev lua-ldoc zip \ - ca-certificates curl libpcre3 libcurl4-openssl-dev \ - libhiredis-dev git build-essential libpcre3-dev luarocks wget - - get-cmake - - build-sdk - - run: - name: Run tests - command: | - lua<< parameters.lua-version >> test.lua diff --git a/LICENSE b/LICENSE index f711cb7..ab8bd33 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2020 Catamorphic, Co. +Copyright 2023 Catamorphic, Co. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From efd43ec6fdf01d7cbcff5d6f669a3fed29a00598 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 11 Dec 2023 14:59:58 -0800 Subject: [PATCH 064/161] update README badges --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 19febd1..df28448 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ LaunchDarkly Server-Side SDK for Lua =========================== -[![CircleCI](https://circleci.com/gh/launchdarkly/lua-server-sdk.svg?style=svg)](https://circleci.com/gh/launchdarkly/lua-server-sdk) +[![Actions Status](https://github.com/launchdarkly/lua-server-sdk-private/actions/workflows/ci.yml/badge.svg)](https://github.com/launchdarkly/lua-server-sdk-private/actions/workflows/ci.yml) +[![Documentation](https://img.shields.io/static/v1?label=GitHub+Pages&message=API+reference&color=00add8)](https://launchdarkly.github.io/lua-server-sdk-private) LaunchDarkly overview ------------------------- From 4d9753840006653bd9c74df59db06c948c440d3a Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 11 Dec 2023 15:00:11 -0800 Subject: [PATCH 065/161] update extra-files with verison --- launchdarkly-server-sdk-1.0-0.rockspec | 2 +- launchdarkly-server-sdk.c | 2 +- release-please-config.json | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/launchdarkly-server-sdk-1.0-0.rockspec b/launchdarkly-server-sdk-1.0-0.rockspec index 19f31e6..78323f1 100644 --- a/launchdarkly-server-sdk-1.0-0.rockspec +++ b/launchdarkly-server-sdk-1.0-0.rockspec @@ -4,7 +4,7 @@ version = "1.0-0" source = { url = "git+https://github.com/launchdarkly/lua-server-sdk.git", - tag = "1.0.0" + tag = "1.2.2" -- {{ x-release-please-version }} } dependencies = { diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index d5442e5..4fed17b 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -11,7 +11,7 @@ Server-side SDK for LaunchDarkly. #include -#define SDKVersion "1.2.2" // {x-release-please-version} +#define SDKVersion "1.2.2" /* {x-release-please-version} */ static struct LDJSON * LuaValueToJSON(lua_State *const l, const int i); diff --git a/release-please-config.json b/release-please-config.json index dc2a6fa..67ccd2b 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -4,10 +4,9 @@ "release-type": "simple", "bump-minor-pre-major": true, "versioning": "default", - "//Delete this 3": "extra-files is only needed if your release is not supported, or you have extra files that need the version number. Delete the entire node if you don't need it.", "extra-files": [ - "demo_version.xml", - "demo_version.txt" + "launchdarkly-server-sdk.c", + "launchdarkly-server-sdk-1.0-0.rockspec" ] } } From e5f3856d29bd9b0f90d422281796909b7e0995f7 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 11 Dec 2023 15:07:43 -0800 Subject: [PATCH 066/161] use public repo links in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index df28448..e057d9a 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ LaunchDarkly Server-Side SDK for Lua =========================== -[![Actions Status](https://github.com/launchdarkly/lua-server-sdk-private/actions/workflows/ci.yml/badge.svg)](https://github.com/launchdarkly/lua-server-sdk-private/actions/workflows/ci.yml) -[![Documentation](https://img.shields.io/static/v1?label=GitHub+Pages&message=API+reference&color=00add8)](https://launchdarkly.github.io/lua-server-sdk-private) +[![Actions Status](https://github.com/launchdarkly/lua-server-sdk-private/actions/workflows/ci.yml/badge.svg)](https://github.com/launchdarkly/lua-server-sdk/actions/workflows/ci.yml) +[![Documentation](https://img.shields.io/static/v1?label=GitHub+Pages&message=API+reference&color=00add8)](https://launchdarkly.github.io/lua-server-sdk) LaunchDarkly overview ------------------------- From 12e46da2fbf147479b297916d79dee973aac2b11 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 12 Dec 2023 10:56:51 -0800 Subject: [PATCH 067/161] Update .github/actions/ci/action.yml Co-authored-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> --- .github/actions/ci/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index b2a0d57..a1622b5 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -2,7 +2,7 @@ name: CI Workflow description: 'Shared CI workflow.' inputs: lua-version: - description: 'Which version of Lua to use?' + description: 'Version of Lua to use for building and testing.' required: true runs: From baec337f266cc66712107202b0ec63094331a39f Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 12 Dec 2023 11:23:12 -0800 Subject: [PATCH 068/161] revert copyright change --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index ab8bd33..f711cb7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2023 Catamorphic, Co. +Copyright 2020 Catamorphic, Co. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From afbece99942ef446fefa949494ccc736a943ea4f Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 12 Dec 2023 11:25:11 -0800 Subject: [PATCH 069/161] Use SHAs instead of version tags --- .github/actions/ci/action.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index a1622b5..af17909 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -8,10 +8,10 @@ inputs: runs: using: composite steps: - - uses: leafo/gh-actions-lua@v10.0.0 + - uses: leafo/gh-actions-lua@35bcb06abec04ec87df82e08caa84d545348536e with: luaVersion: ${{ inputs.lua-version }} - - uses: leafo/gh-actions-luarocks@v4.3.0 + - uses: leafo/gh-actions-luarocks@e65774a6386cb4f24e293dca7fc4ff89165b64c5 - name: Install C Server-side SDK Dependencies shell: bash run: | From abc7f650ff44936323e9faa959879eca377c0670 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 12 Dec 2023 14:59:28 -0800 Subject: [PATCH 070/161] fetch cpp server sdk v3 --- scripts/build-c-server.sh | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/scripts/build-c-server.sh b/scripts/build-c-server.sh index a5abfa7..571ff97 100755 --- a/scripts/build-c-server.sh +++ b/scripts/build-c-server.sh @@ -1,11 +1,21 @@ #!/bin/bash +# Usage: ./scripts/build-c-server.sh + set -e -git clone https://github.com/launchdarkly/c-server-sdk.git +branch="main" + +if [ -z "$1" ] +then + branch="$1" +fi + + +git clone --depth 1 --branch "$branch" https://github.com/launchdarkly/cpp-sdks.git cd c-server-sdk mkdir build cd build -cmake -D BUILD_SHARED_LIBS=ON -D BUILD_TESTING=OFF -D REDIS_STORE=ON .. -make -sudo make install +cmake -D LD_BUILD_SHARED_LIBS=ON -D BUILD_TESTING=OFF -D LD_BUILD_EXAMPLES=OFF-D LD_BUILD_REDIS_SUPPORT=ON .. +cmake --build . --target launchdarkly-cpp-server-redis-source +cmake --install . From e1c540092cbb005585c4cd85119c46c965bc2894 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 12 Dec 2023 16:32:41 -0800 Subject: [PATCH 071/161] updating bindings --- .github/actions/ci/action.yml | 2 +- README.md | 7 +- launchdarkly-server-sdk-1.0-0.rockspec | 4 +- launchdarkly-server-sdk-redis-1.0-0.rockspec | 4 +- launchdarkly-server-sdk-redis.c | 4 +- launchdarkly-server-sdk.c | 202 +++++++++---------- scripts/build-c-server.sh | 11 +- 7 files changed, 113 insertions(+), 121 deletions(-) diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index af17909..20e3b1e 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -21,7 +21,7 @@ runs: - name: Setup C Server-side SDK shell: bash - run: ./scripts/build-c-server.sh + run: ./scripts/build-c-server.sh launchdarkly-cpp-server-redis-source-v1.0.0 - name: Build Lua Server-side SDK shell: bash diff --git a/README.md b/README.md index e057d9a..53aaeb8 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,12 @@ This version of the LaunchDarkly SDK is compatible with the Lua 5.1-5.3 interpre Supported C server-side SDK versions ----------- -This version of the Lua server-side SDK depends on the C server-side SDK. The minimum required version is `2.1.0`, and under `3.0.0`. +This version of the Lua server-side SDK depends on the C++ server-side SDK C bindings, as well as the +C++ server-side SDK's Redis source integration. + +The minimum required version is `1.0.0` for the Redis source. + + Getting started ----------- diff --git a/launchdarkly-server-sdk-1.0-0.rockspec b/launchdarkly-server-sdk-1.0-0.rockspec index 78323f1..24325e1 100644 --- a/launchdarkly-server-sdk-1.0-0.rockspec +++ b/launchdarkly-server-sdk-1.0-0.rockspec @@ -13,7 +13,7 @@ dependencies = { external_dependencies = { LD = { - header = "launchdarkly/api.h" + header = "launchdarkly/server_side/bindings/c/sdk.h" } } @@ -24,7 +24,7 @@ build = { sources = { "launchdarkly-server-sdk.c" }, incdirs = {"$(LD_INCDIR)"}, libdirs = {"$(LD_LIBDIR)"}, - libraries = {"ldserverapi"} + libraries = {"launchdarkly-cpp-server"} } } } diff --git a/launchdarkly-server-sdk-redis-1.0-0.rockspec b/launchdarkly-server-sdk-redis-1.0-0.rockspec index 07ef6b2..5305893 100644 --- a/launchdarkly-server-sdk-redis-1.0-0.rockspec +++ b/launchdarkly-server-sdk-redis-1.0-0.rockspec @@ -12,7 +12,7 @@ dependencies = { external_dependencies = { LDREDIS = { - header = "launchdarkly/store/redis.h" + header = "launchdarkly/server_side/integrations/redis/redis_source.hpp" } } @@ -23,7 +23,7 @@ build = { sources = { "launchdarkly-server-sdk-redis.c" }, incdirs = {"$(LDREDIS_INCDIR)"}, libdirs = {"$(LDREDIS_LIBDIR)"}, - libraries = {"ldserverapi-redis"} + libraries = {"launchdarkly-cpp-server-redis-source"} } } } diff --git a/launchdarkly-server-sdk-redis.c b/launchdarkly-server-sdk-redis.c index 606b63b..d6282e9 100644 --- a/launchdarkly-server-sdk-redis.c +++ b/launchdarkly-server-sdk-redis.c @@ -9,8 +9,8 @@ Server-side SDK for LaunchDarkly Redis store. #include #include -#include -#include +#include +#include /*** Initialize a store backend diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 4fed17b..2f693d9 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -9,7 +9,15 @@ Server-side SDK for LaunchDarkly. #include #include -#include +#include +#include + +#include +#include +#include +#include + + #define SDKVersion "1.2.2" /* {x-release-please-version} */ @@ -23,44 +31,22 @@ static struct LDJSON * LuaArrayToJSON(lua_State *const l, const int i); static void -LuaPushJSON(lua_State *const l, const struct LDJSON *const j); +LuaPushJSON(lua_State *const l, LDValue j); static int globalLoggingCallback; static lua_State *globalLuaState; static void -logHandler(const LDLogLevel level, const char * const line) +logHandler(const enum LDLogLevel level, const char * const line) { lua_rawgeti(globalLuaState, LUA_REGISTRYINDEX, globalLoggingCallback); - lua_pushstring(globalLuaState, LDLogLevelToString(level)); + lua_pushstring(globalLuaState, LDLogLevel_Name(level, "unknown")); lua_pushstring(globalLuaState, line); lua_call(globalLuaState, 2, 0); } -static LDLogLevel -LuaStringToLogLevel(const char *const text) -{ - if (strcmp(text, "FATAL") == 0) { - return LD_LOG_FATAL; - } else if (strcmp(text, "CRITICAL") == 0) { - return LD_LOG_CRITICAL; - } else if (strcmp(text, "ERROR") == 0) { - return LD_LOG_ERROR; - } else if (strcmp(text, "WARNING") == 0) { - return LD_LOG_WARNING; - } else if (strcmp(text, "INFO") == 0) { - return LD_LOG_INFO; - } else if (strcmp(text, "DEBUG") == 0) { - return LD_LOG_DEBUG; - } else if (strcmp(text, "TRACE") == 0) { - return LD_LOG_TRACE; - } - - return LD_LOG_INFO; -} - /*** Set the global logger for all SDK operations. This function is not thread safe, and if used should be done so before other operations. The default @@ -83,7 +69,8 @@ LuaLDRegisterLogger(lua_State *const l) globalLuaState = l; globalLoggingCallback = luaL_ref(l, LUA_REGISTRYINDEX); - LDConfigureGlobalLogger(LuaStringToLogLevel(level), logHandler); +// TODO: Install custom loggger + // LDConfigureGlobalLogger(LDLogLevel_Enum(level, LD_LOG_INFO), logHandler); return 0; } @@ -110,20 +97,20 @@ isArray(lua_State *const l, const int i) return array; } -static struct LDJSON * +static LDValue LuaValueToJSON(lua_State *const l, const int i) { - struct LDJSON *result; + LDValue result = NULL; switch (lua_type(l, i)) { case LUA_TBOOLEAN: - result = LDNewBool(lua_toboolean(l, i)); + result = LDValue_NewBool(lua_toboolean(l, i)); break; case LUA_TNUMBER: - result = LDNewNumber(lua_tonumber(l, i)); + result = LDValue_NewNumber(lua_tonumber(l, i)); break; case LUA_TSTRING: - result = LDNewText(lua_tostring(l, i)); + result = LDValue_NewString(lua_tostring(l, i)); break; case LUA_TTABLE: if (isArray(l, i)) { @@ -133,39 +120,39 @@ LuaValueToJSON(lua_State *const l, const int i) } break; default: - result = LDNewNull(); + result = LDValue_NewNull(); break; } return result; } -static struct LDJSON * +static LDValue LuaArrayToJSON(lua_State *const l, const int i) { - struct LDJSON *const result = LDNewArray(); + LDArrayBuilder result = LDArrayBuilder_New(); lua_pushvalue(l, i); lua_pushnil(l); while (lua_next(l, -2) != 0) { - struct LDJSON *value = LuaValueToJSON(l, -1); + LDValue value = LuaValueToJSON(l, -1); - LDArrayPush(result, value); + LDArrayBuilder_Add(result, value); lua_pop(l, 1); } lua_pop(l, 1); - return result; + return LDArrayBuilder_Build(result); } static struct LDJSON * LuaTableToJSON(lua_State *const l, const int i) { - struct LDJSON *const result = LDNewObject(); + LDObjectBuilder result = LDObjectBuilder_New(); lua_pushvalue(l, i); @@ -173,64 +160,64 @@ LuaTableToJSON(lua_State *const l, const int i) while (lua_next(l, -2) != 0) { const char *const key = lua_tostring(l, -2); - struct LDJSON *const value = LuaValueToJSON(l, -1); + LDValue value = LuaValueToJSON(l, -1); - LDObjectSetKey(result, key, value); + LDObjectBuilder_Add(result, key, value); lua_pop(l, 1); } lua_pop(l, 1); - return result; + return LDObjectBuilder_Build(result); } static void -LuaPushJSONObject(lua_State *const l, const struct LDJSON *const j) +LuaPushJSONObject(lua_State *const l, LDValue j) { - struct LDJSON *iter; + struct LDValue_ObjectIter iter; lua_newtable(l); - for (iter = LDGetIter(j); iter; iter = LDIterNext(iter)) { - LuaPushJSON(l, iter); - lua_setfield(l, -2, LDIterKey(iter)); + for (iter = LDValue_ObjectIter_New(j); !LDValue_ObjectIter_End(iter); LDValue_ObjectIter_Next(iter)) { + LuaPushJSON(l, LDValue_ObjectIter_Value(iter)); + lua_setfield(l, -2, LDValue_ObjectIter_Key(iter)); } } static void LuaPushJSONArray(lua_State *const l, const struct LDJSON *const j) { - struct LDJSON *iter; + struct LDValue_ArrayIter iter; lua_newtable(l); int index = 1; - for (iter = LDGetIter(j); iter; iter = LDIterNext(iter)) { - LuaPushJSON(l, iter); + for (iter = LDValue_ArrayIter_New(j); !LDValue_ArrayIter_End(iter); LDValue_ArrayIter_Next(iter)) { + LuaPushJSON(l, LDValue_ArrayIter_Value(iter)); lua_rawseti(l, -2, index); index++; } } static void -LuaPushJSON(lua_State *const l, const struct LDJSON *const j) +LuaPushJSON(lua_State *const l, LDValue j) { - switch (LDJSONGetType(j)) { - case LDText: - lua_pushstring(l, LDGetText(j)); + switch (LDValue_Type(j)) { + case LDValueType_String: + lua_pushstring(l, LDValue_GetString(j)); break; - case LDBool: - lua_pushboolean(l, LDGetBool(j)); + case LDValueType_Bool: + lua_pushboolean(l, LDValue_GetBool(j)); break; - case LDNumber: - lua_pushnumber(l, LDGetNumber(j)); + case LDValueType_Number: + lua_pushnumber(l, LDValue_GetNumber(j)); break; - case LDObject: + case LDValueType_Object: LuaPushJSONObject(l, j); break; - case LDArray: + case LDValueType_Array: LuaPushJSONArray(l, j); break; default: @@ -273,86 +260,80 @@ LuaLDUserNew(lua_State *const l) const char *const key = luaL_checkstring(l, -1); - struct LDUser *user = LDUserNew(key); + LDContextBuilder builder = LDContextBuilder_New(); + + LDContextBuilder_AddKind(builder, "user", key); lua_getfield(l, 1, "anonymous"); if (lua_isboolean(l, -1)) { - LDUserSetAnonymous(user, lua_toboolean(l, -1)); + LDContextBuilder_Attributes_SetAnonymous(builder, "user", true); } lua_getfield(l, 1, "ip"); if (lua_isstring(l, -1)) { - LDUserSetIP(user, luaL_checkstring(l,-1)); + LDContextBuilder_Attributes_Set(builder, "user", "ip", LDValue_NewString(luaL_checkstring(l, -1))); }; lua_getfield(l, 1, "firstName"); if (lua_isstring(l, -1)) { - LDUserSetFirstName(user, luaL_checkstring(l,-1)); + LDContextBuilder_Attributes_Set(builder, "user", "firstName", LDValue_NewString(luaL_checkstring(l, -1))); } lua_getfield(l, 1, "lastName"); if (lua_isstring(l, -1)) { - LDUserSetLastName(user, luaL_checkstring(l, -1)); + LDContextBuilder_Attributes_Set(builder, "user", "lastName", LDValue_NewString(luaL_checkstring(l, -1))); } lua_getfield(l, 1, "email"); if (lua_isstring(l, -1)) { - LDUserSetEmail(user, luaL_checkstring(l, -1)); + LDContextBuilder_Attributes_Set(builder, "user", "email", LDValue_NewString(luaL_checkstring(l, -1))); } lua_getfield(l, 1, "name"); if (lua_isstring(l, -1)) { - LDUserSetName(user, luaL_checkstring(l, -1)); + LDContextBuilder_Attributes_SetName(builder, "user", luaL_checkstring(l, -1)); } lua_getfield(l, 1, "avatar"); if (lua_isstring(l, -1)) { - LDUserSetAvatar(user, luaL_checkstring(l, -1)); + LDContextBuilder_Attributes_Set(builder, "user", "avatar", LDValue_NewString(luaL_checkstring(l, -1))); } lua_getfield(l, 1, "country"); if (lua_isstring(l, -1)) { - LDUserSetCountry(user, luaL_checkstring(l, -1)); - } - - lua_getfield(l, 1, "secondary"); - - if (lua_isstring(l, -1)) { - LDUserSetSecondary(user, luaL_checkstring(l, -1)); + LDContextBuilder_Attributes_Set(builder, "user", "country", LDValue_NewString(luaL_checkstring(l, -1))); } lua_getfield(l, 1, "custom"); if (lua_istable(l, -1)) { - LDUserSetCustom(user, LuaValueToJSON(l, -1)); + // TODO: Iterate through the props and add as individual attributes } lua_getfield(l, 1, "privateAttributeNames"); if (lua_istable(l, -1)) { - struct LDJSON *attrs, *iter; - attrs = LuaValueToJSON(l, -1); - for (iter = LDGetIter(attrs); iter; iter = LDIterNext(iter)) { - LDUserAddPrivateAttribute(user, LDGetText(iter)); - } + // TODO: Iterate throughthe table and add the attribute names + LDContextBuilder_Attributes_AddPrivateAttribute(builder, "user", "foo"); - LDJSONFree(attrs); } - struct LDUser **u = (struct LDUser **)lua_newuserdata(l, sizeof(user)); + LDContext context = LDContextBuilder_Build(builder); - *u = user; + LDContext *u = (LDContext *)lua_newuserdata(l, sizeof(context)); - luaL_getmetatable(l, "LaunchDarklyUser"); + *u = context; + + luaL_getmetatable(l, "LaunchDarklyContext"); lua_setmetatable(l, -2); return 1; @@ -374,13 +355,13 @@ LuaLDVersion(lua_State *const l) static int LuaLDUserFree(lua_State *const l) { - struct LDUser **user; + LDContext *context; - user = (struct LDUser **)luaL_checkudata(l, 1, "LaunchDarklyUser"); + context = (struct LDUser **)luaL_checkudata(l, 1, "LaunchDarklyContext"); - if (*user) { - LDUserFree(*user); - *user = NULL; + if (*context) { + LDContext_Free(*context); + *context = NULL; } return 0; @@ -501,7 +482,7 @@ makeConfig(lua_State *const l, const int i) attrs = LuaValueToJSON(l, -1); for (iter = LDGetIter(attrs); iter; iter = LDIterNext(iter)) { - LDConfigAddPrivateAttribute(config, LDGetText(iter)); + LDConfigAddPrivateAttribute(config, LDValue_GetString(iter)); } LDJSONFree(attrs); @@ -571,8 +552,8 @@ returned. static int LuaLDClientInit(lua_State *const l) { - struct LDClient *client; - struct LDConfig *config; + LDServerSDK client; + LDConfig config; unsigned int timeout; if (lua_gettop(l) != 2) { @@ -581,12 +562,13 @@ LuaLDClientInit(lua_State *const l) config = makeConfig(l, 1); + // TODO: use timeout timeout = luaL_checkinteger(l, 2); - client = LDClientInit(config, timeout); + client = LDServerSDK_New(config); - struct LDClient **c = - (struct LDClient **)lua_newuserdata(l, sizeof(client)); + struct LDServerSDK *c = + (struct LDServerSDK *)lua_newuserdata(l, sizeof(client)); *c = client; @@ -599,12 +581,12 @@ LuaLDClientInit(lua_State *const l) static int LuaLDClientClose(lua_State *const l) { - struct LDClient **client; + LDServerSDK *client; - client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + client = (LDServerSDK *)luaL_checkudata(l, 1, "LaunchDarklyClient"); - if (*client) { - LDClientClose(*client); + if (client) { + LDServerSDK_Free(*client); *client = NULL; } @@ -649,23 +631,23 @@ Evaluate a boolean flag static int LuaLDClientBoolVariation(lua_State *const l) { - struct LDClient **client; - struct LDUser **user; + LDServerSDK *client; + LDContext *context; if (lua_gettop(l) != 4) { return luaL_error(l, "expecting exactly 4 arguments"); } - client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + client = (LDServerSDK *)luaL_checkudata(l, 1, "LaunchDarklyClient"); - user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); + context = (LDContext *)luaL_checkudata(l, 2, "LaunchDarklyContext"); const char *const key = luaL_checkstring(l, 3); const int fallback = lua_toboolean(l, 4); - const LDBoolean result = - LDBoolVariation(*client, *user, key, fallback, NULL); + const bool result = + LDServerSDK_BoolVariation(*client, *context, key, fallback); lua_pushboolean(l, result); @@ -705,7 +687,7 @@ LuaLDClientBoolVariationDetail(lua_State *const l) const LDBoolean result = LDBoolVariation(*client, *user, key, fallback, &details); - LuaPushDetails(l, &details, LDNewBool(result)); + LuaPushDetails(l, &details, LDValue_NewBool(result)); return 1; } @@ -776,7 +758,7 @@ LuaLDClientIntVariationDetail(lua_State *const l) const int result = LDIntVariation(*client, *user, key, fallback, &details); - LuaPushDetails(l, &details, LDNewNumber(result)); + LuaPushDetails(l, &details, LDValue_NewNumber(result)); return 1; } @@ -849,7 +831,7 @@ LuaLDClientDoubleVariationDetail(lua_State *const l) const double result = LDDoubleVariation(*client, *user, key, fallback, &details); - LuaPushDetails(l, &details, LDNewNumber(result)); + LuaPushDetails(l, &details, LDValue_NewNumber(result)); return 1; } @@ -924,7 +906,7 @@ LuaLDClientStringVariationDetail(lua_State *const l) char *const result = LDStringVariation(*client, *user, key, fallback, &details); - LuaPushDetails(l, &details, LDNewText(result)); + LuaPushDetails(l, &details, LDValue_NewString(result)); LDFree(result); diff --git a/scripts/build-c-server.sh b/scripts/build-c-server.sh index 571ff97..4cda090 100755 --- a/scripts/build-c-server.sh +++ b/scripts/build-c-server.sh @@ -6,16 +6,21 @@ set -e branch="main" -if [ -z "$1" ] +if [ -n "$1" ] then branch="$1" fi git clone --depth 1 --branch "$branch" https://github.com/launchdarkly/cpp-sdks.git -cd c-server-sdk +cd cpp-sdks mkdir build cd build -cmake -D LD_BUILD_SHARED_LIBS=ON -D BUILD_TESTING=OFF -D LD_BUILD_EXAMPLES=OFF-D LD_BUILD_REDIS_SUPPORT=ON .. +cmake -D LD_BUILD_SHARED_LIBS=ON \ + -D BUILD_TESTING=OFF \ + -D LD_BUILD_EXAMPLES=OFF \ + -D LD_BUILD_REDIS_SUPPORT=ON \ + -D CMAKE_INSTALL_PREFIX=./INSTALL .. + cmake --build . --target launchdarkly-cpp-server-redis-source cmake --install . From daad0d6eb388090573b1d598f2ad3cbbf1286a1b Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 13 Dec 2023 11:10:47 -0800 Subject: [PATCH 072/161] updating to use new API, many compiler errors --- launchdarkly-server-sdk.c | 351 ++++++++++++++++++++------------------ 1 file changed, 183 insertions(+), 168 deletions(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 2f693d9..5bf753e 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -21,13 +21,13 @@ Server-side SDK for LaunchDarkly. #define SDKVersion "1.2.2" /* {x-release-please-version} */ -static struct LDJSON * +static LDValue LuaValueToJSON(lua_State *const l, const int i); -static struct LDJSON * +static LDValue LuaTableToJSON(lua_State *const l, const int i); -static struct LDJSON * +static LDValue LuaArrayToJSON(lua_State *const l, const int i); static void @@ -36,6 +36,15 @@ LuaPushJSON(lua_State *const l, LDValue j); static int globalLoggingCallback; static lua_State *globalLuaState; +static int lua_tablelen(lua_State *L, int index) +{ + #if LUA_VERSION_NUM >= 502 + return lua_rawlen(L, index); + #else + return lua_objlen(L, index); + #endif +} + static void logHandler(const enum LDLogLevel level, const char * const line) { @@ -69,7 +78,7 @@ LuaLDRegisterLogger(lua_State *const l) globalLuaState = l; globalLoggingCallback = luaL_ref(l, LUA_REGISTRYINDEX); -// TODO: Install custom loggger + // TODO: Install custom loggger // LDConfigureGlobalLogger(LDLogLevel_Enum(level, LD_LOG_INFO), logHandler); return 0; @@ -149,7 +158,7 @@ LuaArrayToJSON(lua_State *const l, const int i) return LDArrayBuilder_Build(result); } -static struct LDJSON * +static LDValue LuaTableToJSON(lua_State *const l, const int i) { LDObjectBuilder result = LDObjectBuilder_New(); @@ -175,7 +184,7 @@ LuaTableToJSON(lua_State *const l, const int i) static void LuaPushJSONObject(lua_State *const l, LDValue j) { - struct LDValue_ObjectIter iter; + LDValue_ObjectIter iter; lua_newtable(l); @@ -186,9 +195,9 @@ LuaPushJSONObject(lua_State *const l, LDValue j) } static void -LuaPushJSONArray(lua_State *const l, const struct LDJSON *const j) +LuaPushJSONArray(lua_State *const l, LDValue j) { - struct LDValue_ArrayIter iter; + LDValue_ArrayIter iter; lua_newtable(l); @@ -312,19 +321,47 @@ LuaLDUserNew(lua_State *const l) LDContextBuilder_Attributes_Set(builder, "user", "country", LDValue_NewString(luaL_checkstring(l, -1))); } + + lua_getfield(l, 1, "secondary"); + + if (lua_isstring(l, -1)) { + // TODO: Log a warning that this was removed + + + } + lua_getfield(l, 1, "custom"); + // The individual fields of custom are added to the top-level of the context. + // TODO: document this. if (lua_istable(l, -1)) { - // TODO: Iterate through the props and add as individual attributes + lua_pushnil(l); + + while (lua_next(l, -2) != 0) { + const char *const key = luaL_checkstring(l, -2); + + LDValue value = LuaValueToJSON(l, -1); + + LDContextBuilder_Attributes_Set(builder, "user", key, value); + + lua_pop(l, 1); + } } lua_getfield(l, 1, "privateAttributeNames"); if (lua_istable(l, -1)) { + int n = lua_tablelen(l, -1); - // TODO: Iterate throughthe table and add the attribute names - LDContextBuilder_Attributes_AddPrivateAttribute(builder, "user", "foo"); + for (int i = 1; i <= n; i++) { + lua_rawgeti(l, -1, i); + if (lua_isstring(l, -1)) { + LDContextBuilder_Attributes_AddPrivateAttribute(builder, "user", luaL_checkstring(l, -1)); + } + + lua_pop(l, 1); + } } LDContext context = LDContextBuilder_Build(builder); @@ -352,12 +389,16 @@ LuaLDVersion(lua_State *const l) return 1; } +/** +Frees a user object. +@deprecated Users are deprecated. Use contexts instead. +*/ static int LuaLDUserFree(lua_State *const l) { LDContext *context; - context = (struct LDUser **)luaL_checkudata(l, 1, "LaunchDarklyContext"); + context = (LDContext *)luaL_checkudata(l, 1, "LaunchDarklyContext"); if (*context) { LDContext_Free(*context); @@ -367,10 +408,10 @@ LuaLDUserFree(lua_State *const l) return 0; } -static struct LDConfig * +static LDServerConfig makeConfig(lua_State *const l, const int i) { - struct LDConfig *config; + LDServerConfigBuilder builder; luaL_checktype(l, i, LUA_TTABLE); @@ -378,119 +419,132 @@ makeConfig(lua_State *const l, const int i) const char *const key = luaL_checkstring(l, -1); - config = LDConfigNew(key); + builder = LDServerConfigBuilder_New(key); lua_getfield(l, i, "baseURI"); if (lua_isstring(l, -1)) { - LDConfigSetBaseURI(config, luaL_checkstring(l, -1)); + LDServerConfigBuilder_ServiceEndpoints_PollingBaseURL(builder, luaL_checkstring(l, -1)); } lua_getfield(l, i, "streamURI"); if (lua_isstring(l, -1)) { - LDConfigSetStreamURI(config, luaL_checkstring(l, -1)); + LDServerConfigBuilder_ServiceEndpoints_StreamingBaseURL(builder, luaL_checkstring(l, -1)); } lua_getfield(l, i, "eventsURI"); if (lua_isstring(l, -1)) { - LDConfigSetEventsURI(config, luaL_checkstring(l, -1)); + LDServerConfigBuilder_ServiceEndpoints_EventsBaseURL(builder, luaL_checkstring(l, -1)); } lua_getfield(l, i, "stream"); if (lua_isboolean(l, -1)) { - LDConfigSetStream(config, lua_toboolean(l, -1)); + // TODO: Switch between poll/streaming data systems + //LDConfigSetStream(config, lua_toboolean(l, -1)); } lua_getfield(l, i, "sendEvents"); - if (lua_isstring(l, -1)) { - LDConfigSetSendEvents(config, lua_toboolean(l, -1)); + if (lua_isboolean(l, -1)) { + LDServerConfigBuilder_Events_Enabled(builder, lua_toboolean(l, -1)); } lua_getfield(l, i, "eventsCapacity"); if (lua_isnumber(l, -1)) { - LDConfigSetEventsCapacity(config, luaL_checkinteger(l, -1)); + LDServerConfigBuilder_Events_Capacity(builder, luaL_checkinteger(l, -1)); } lua_getfield(l, i, "timeout"); if (lua_isnumber(l, -1)) { - LDConfigSetTimeout(config, luaL_checkinteger(l, -1)); + // TODO: Use timeout } lua_getfield(l, i, "flushInterval"); if (lua_isnumber(l, -1)) { - LDConfigSetFlushInterval(config, luaL_checkinteger(l, -1)); + LDServerConfigBuilder_Events_FlushIntervalMs(builder, luaL_checkinteger(l, -1)); } lua_getfield(l, i, "pollInterval"); if (lua_isnumber(l, -1)) { - LDConfigSetPollInterval(config, luaL_checkinteger(l, -1)); + // TODO: Set poll data system } lua_getfield(l, i, "offline"); if (lua_isboolean(l, -1)) { - LDConfigSetOffline(config, lua_toboolean(l, -1)); + // TODO: Document change in behavior (offline now disables events + data system) + LDServerConfigBuilder_Offline(builder, lua_toboolean(l, -1)); } lua_getfield(l, i, "useLDD"); if (lua_isboolean(l, -1)) { - LDConfigSetUseLDD(config, lua_toboolean(l, -1)); + // TODO: Warn that this needs to be setup } lua_getfield(l, i, "inlineUsersInEvents"); if (lua_isboolean(l, -1)) { - LDConfigInlineUsersInEvents(config, lua_toboolean(l, -1)); + // TODO: warn this was removed } lua_getfield(l, i, "allAttributesPrivate"); if (lua_isboolean(l, -1)) { - LDConfigSetAllAttributesPrivate(config, lua_toboolean(l, -1)); + LDServerConfigBuilder_Events_AllAttributesPrivate(builder, lua_toboolean(l, -1)); } lua_getfield(l, i, "userKeysCapacity"); if (lua_isnumber(l, -1)) { - LDConfigSetUserKeysCapacity(config, luaL_checkinteger(l, -1)); + // TODO: We don't have a C binding for this yet + //LDConfigSetUserKeysCapacity(config, luaL_checkinteger(l, -1)); } lua_getfield(l, i, "featureStoreBackend"); if (lua_isuserdata(l, -1)) { - struct LDStoreInterface **storeInterface; - - storeInterface = (struct LDStoreInterface **) - luaL_checkudata(l, -1, "LaunchDarklyStoreInterface"); - - LDConfigSetFeatureStoreBackend(config, *storeInterface); + // TODO: Setup the store reader backend + // struct LDStoreInterface **storeInterface; + // + // storeInterface = (struct LDStoreInterface **) + // luaL_checkudata(l, -1, "LaunchDarklyStoreInterface"); + // + // LDConfigSetFeatureStoreBackend(config, *storeInterface); } lua_getfield(l, 1, "privateAttributeNames"); if (lua_istable(l, -1)) { - struct LDJSON *attrs, *iter; - attrs = LuaValueToJSON(l, -1); + int n = lua_tablelen(l, -1); - for (iter = LDGetIter(attrs); iter; iter = LDIterNext(iter)) { - LDConfigAddPrivateAttribute(config, LDValue_GetString(iter)); - } + for (int i = 1; i <= n; i++) { + lua_rawgeti(l, -1, i); - LDJSONFree(attrs); + if (lua_isstring(l, -1)) { + LDServerConfigBuilder_Events_PrivateAttribute(builder, luaL_checkstring(l, -1)); + } + + lua_pop(l, 1); + } } - LDConfigSetWrapperInfo(config, "lua-server-sdk", SDKVersion); + LDServerConfigBuilder_HttpProperties_WrapperName(builder, "lua-server-sdk"); + LDServerConfigBuilder_HttpProperties_WrapperVersion(builder, SDKVersion); + - return config; + LDServerConfig out_config; + LDServerConfigBuilder_Build(builder, &out_config); + + // TODO: Check result of the call + return out_config; } /*** @@ -553,7 +607,7 @@ static int LuaLDClientInit(lua_State *const l) { LDServerSDK client; - LDConfig config; + LDServerConfig config; unsigned int timeout; if (lua_gettop(l) != 2) { @@ -567,8 +621,7 @@ LuaLDClientInit(lua_State *const l) client = LDServerSDK_New(config); - struct LDServerSDK *c = - (struct LDServerSDK *)lua_newuserdata(l, sizeof(client)); + LDServerSDK *c = (LDServerSDK *) lua_newuserdata(l, sizeof(client)); *c = client; @@ -594,29 +647,24 @@ LuaLDClientClose(lua_State *const l) } static void -LuaPushDetails(lua_State *const l, struct LDDetails *const details, - struct LDJSON *const value) +LuaPushDetails(lua_State *const l, LDEvalDetail details, + LDValue value) { - struct LDJSON *reason; - - reason = LDReasonToJSON(details); - lua_newtable(l); + /** TODO: C bindings for this? */ - LuaPushJSON(l, reason); lua_setfield(l, -2, "reason"); if (details->hasVariation) { - lua_pushnumber(l, details->variationIndex); + lua_pushnumber(l, LDEvalDetail_VariationIndex(details)); lua_setfield(l, -2, "variationIndex"); } LuaPushJSON(l, value); lua_setfield(l, -2, "value"); - LDDetailsClear(details); - LDJSONFree(value); - LDJSONFree(reason); + LDEvalDetail_Free(details); + LDValue_Free(value); } /** @@ -666,9 +714,10 @@ Evaluate a boolean flag and return an explanation static int LuaLDClientBoolVariationDetail(lua_State *const l) { - struct LDClient **client; - struct LDUser **user; - struct LDDetails details; + LDServerSDK *client; + LDContext *context; + + LDEvalDetail details; if (lua_gettop(l) != 4) { return luaL_error(l, "expecting exactly 4 arguments"); @@ -676,16 +725,16 @@ LuaLDClientBoolVariationDetail(lua_State *const l) LDDetailsInit(&details); - client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + client = (LDServerSDK *)luaL_checkudata(l, 1, "LaunchDarklyClient"); - user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); + LDContext *context = (LDContext *) luaL_checkudata(l, 2, "LaunchDarklyContext"); const char *const key = luaL_checkstring(l, 3); const int fallback = lua_toboolean(l, 4); - const LDBoolean result = - LDBoolVariation(*client, *user, key, fallback, &details); + const bool result = + LDServerSDK_BoolVariation(*client, *user, key, fallback, &details); LuaPushDetails(l, &details, LDValue_NewBool(result)); @@ -704,22 +753,22 @@ Evaluate an integer flag static int LuaLDClientIntVariation(lua_State *const l) { - struct LDClient **client; - struct LDUser **user; + LDServerSDK *client; + LDContext *context; if (lua_gettop(l) != 4) { return luaL_error(l, "expecting exactly 4 arguments"); } - client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + client = (LDServerSDK *)luaL_checkudata(l, 1, "LaunchDarklyClient"); - user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); + LDContext *context = (LDContext *) luaL_checkudata(l, 2, "LaunchDarklyContext"); const char *const key = luaL_checkstring(l, 3); const int fallback = luaL_checkinteger(l, 4); - const int result = LDIntVariation(*client, *user, key, fallback, NULL); + const int result = LDServerSDK_IntVariation(*client, *user, key, fallback, NULL); lua_pushnumber(l, result); @@ -738,9 +787,9 @@ Evaluate an integer flag and return an explanation static int LuaLDClientIntVariationDetail(lua_State *const l) { - struct LDClient **client; - struct LDUser **user; - struct LDDetails details; + LDServerSDK *client; + LDContext *context; + LDEvalDetail details; if (lua_gettop(l) != 4) { return luaL_error(l, "expecting exactly 4 arguments"); @@ -748,9 +797,9 @@ LuaLDClientIntVariationDetail(lua_State *const l) LDDetailsInit(&details); - client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + client = (LDServerSDK *)luaL_checkudata(l, 1, "LaunchDarklyClient"); - user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); + LDContext *context = (LDContext *) luaL_checkudata(l, 2, "LaunchDarklyContext"); const char *const key = luaL_checkstring(l, 3); @@ -775,16 +824,16 @@ Evaluate a double flag static int LuaLDClientDoubleVariation(lua_State *const l) { - struct LDClient **client; - struct LDUser **user; + LDServerSDK *client; + LDContext *context; if (lua_gettop(l) != 4) { return luaL_error(l, "expecting exactly 4 arguments"); } - client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + client = (LDServerSDK *)luaL_checkudata(l, 1, "LaunchDarklyClient"); - user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); + LDContext *context = (LDContext *) luaL_checkudata(l, 2, "LaunchDarklyContext"); const char *const key = luaL_checkstring(l, 3); @@ -810,9 +859,9 @@ Evaluate a double flag and return an explanation static int LuaLDClientDoubleVariationDetail(lua_State *const l) { - struct LDClient **client; - struct LDUser **user; - struct LDDetails details; + LDServerSDK *client; + LDContext *context; + LDEvalDetail details; if (lua_gettop(l) != 4) { return luaL_error(l, "expecting exactly 4 arguments"); @@ -820,9 +869,9 @@ LuaLDClientDoubleVariationDetail(lua_State *const l) LDDetailsInit(&details); - client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + client = (LDServerSDK *)luaL_checkudata(l, 1, "LaunchDarklyClient"); - user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); + LDContext *context = (LDContext *) luaL_checkudata(l, 2, "LaunchDarklyContext"); const char *const key = luaL_checkstring(l, 3); @@ -848,16 +897,16 @@ Evaluate a string flag static int LuaLDClientStringVariation(lua_State *const l) { - struct LDClient **client; - struct LDUser **user; + LDServerSDK *client; + LDContext *context; if (lua_gettop(l) != 4) { return luaL_error(l, "expecting exactly 4 arguments"); } - client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + client = (LDServerSDK *)luaL_checkudata(l, 1, "LaunchDarklyClient"); - user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); + LDContext *context = (LDContext *) luaL_checkudata(l, 2, "LaunchDarklyContext"); const char *const key = luaL_checkstring(l, 3); @@ -885,9 +934,9 @@ Evaluate a string flag and return an explanation static int LuaLDClientStringVariationDetail(lua_State *const l) { - struct LDClient **client; - struct LDUser **user; - struct LDDetails details; + LDServerSDK *client; + LDContext *context; + LDEvalDetail details; if (lua_gettop(l) != 4) { return luaL_error(l, "expecting exactly 4 arguments"); @@ -895,9 +944,9 @@ LuaLDClientStringVariationDetail(lua_State *const l) LDDetailsInit(&details); - client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + client = (LDServerSDK *)luaL_checkudata(l, 1, "LaunchDarklyClient"); - user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); + LDContext *context = (LDContext *) luaL_checkudata(l, 2, "LaunchDarklyContext"); const char *const key = luaL_checkstring(l, 3); @@ -925,23 +974,23 @@ Evaluate a json flag static int LuaLDClientJSONVariation(lua_State *const l) { - struct LDClient **client; - struct LDUser **user; + LDServerSDK *client; + LDContext *context; struct LDJSON *fallback, *result; if (lua_gettop(l) != 4) { return luaL_error(l, "expecting exactly 4 arguments"); } - client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + client = (LDServerSDK *)luaL_checkudata(l, 1, "LaunchDarklyClient"); - user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); + LDContext *context = (LDContext *) luaL_checkudata(l, 2, "LaunchDarklyContext"); const char *const key = luaL_checkstring(l, 3); fallback = LuaValueToJSON(l, 4); - result = LDJSONVariation(*client, *user, key, fallback, NULL); + result = LDServerSDK_JSONVariation(*client, *user, key, fallback, NULL); LuaPushJSON(l, result); @@ -963,30 +1012,29 @@ Evaluate a json flag and return an explanation static int LuaLDClientJSONVariationDetail(lua_State *const l) { - struct LDClient **client; - struct LDUser **user; - struct LDJSON *fallback, *result; - struct LDDetails details; + LDServerSDK *client; + LDContext *context; + LDValue fallback, result; + + LDEvalDetail details; if (lua_gettop(l) != 4) { return luaL_error(l, "expecting exactly 4 arguments"); } - LDDetailsInit(&details); - - client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + client = (LDServerSDK *)luaL_checkudata(l, 1, "LaunchDarklyClient"); - user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); + context = (LDContext *)luaL_checkudata(l, 2, "LaunchDarklyContext"); const char *const key = luaL_checkstring(l, 3); fallback = LuaValueToJSON(l, 4); - result = LDJSONVariation(*client, *user, key, fallback, &details); + result = LDServerSDK_JSONVariationDetail(*client, *context, key, fallback, &details); - LuaPushDetails(l, &details, result); + LuaPushDetails(l, details, result); - LDJSONFree(fallback); + LDValue_Free(fallback); return 1; } @@ -999,15 +1047,13 @@ Immediately flushes queued events. static int LuaLDClientFlush(lua_State *const l) { - struct LDClient **client; - if (lua_gettop(l) != 1) { return luaL_error(l, "expecting exactly 1 argument"); } - client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + LDServerSDK *client = (LDServerSDK *) luaL_checkudata(l, 1, "LaunchDarklyClient"); - LDClientFlush(*client); + LDServerSDK_Flush(*client, LD_NONBLOCKING); return 0; } @@ -1025,20 +1071,17 @@ can be attached to the event as JSON. static int LuaLDClientTrack(lua_State *const l) { - struct LDClient **client; - struct LDUser **user; - struct LDJSON *value; - if (lua_gettop(l) < 3 || lua_gettop(l) > 5) { return luaL_error(l, "expecting 3-5 arguments"); } - client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + LDServerSDK *client = (LDServerSDK *) luaL_checkudata(l, 1, "LaunchDarklyClient"); const char *const key = luaL_checkstring(l, 2); - user = (struct LDUser **)luaL_checkudata(l, 3, "LaunchDarklyUser"); + LDContext *context = (LDContext *) luaL_checkudata(l, 3, "LaunchDarklyContext"); + LDValue value; if (lua_isnil(l, 4)) { value = NULL; } else { @@ -1048,42 +1091,15 @@ LuaLDClientTrack(lua_State *const l) if (lua_gettop(l) == 5 && lua_isnumber(l, 5)) { const double metric = luaL_checknumber(l, 5); - LDClientTrackMetric(*client, key, *user, value, metric); + LDServerSDK_TrackMetric(*client, *context, key, metric, value); } else { - LDClientTrack(*client, key, *user, value); + LDServerSDK_TrackData(*client, *context, key, value); } return 0; } -/*** -Associates two users for analytics purposes by generating an alias event. -@function alias -@tparam user currentUser An opaque user object from @{makeUser} -@tparam user previousUser An opaque user object from @{makeUser} -@treturn nil -*/ -static int -LuaLDClientAlias(lua_State *const l) -{ - struct LDClient **client; - struct LDUser **currentUser, **previousUser; - struct LDJSON *value; - - if (lua_gettop(l) != 3) { - return luaL_error(l, "expected exactly three arguments"); - } - - client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); - currentUser = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); - previousUser = (struct LDUser **)luaL_checkudata(l, 3, "LaunchDarklyUser"); - - if (!LDClientAlias(*client, *currentUser, *previousUser)) { - return luaL_error(l, "LDClientAlias failed"); - } - - return 0; -} +// TODO: Document alias was removed, use multi context instead /*** Check if a client has been fully initialized. This may be useful if the @@ -1100,9 +1116,9 @@ LuaLDClientIsInitialized(lua_State *const l) return luaL_error(l, "expecting exactly 1 argument"); } - client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + LDServerSDK *client = (LDServerSDK *) luaL_checkudata(l, 1, "LaunchDarklyClient"); - lua_pushboolean(l, LDClientIsInitialized(*client)); + lua_pushboolean(l, LDServerSDK_Initialized(*client)); return 1; } @@ -1116,17 +1132,14 @@ Generates an identify event for a user. static int LuaLDClientIdentify(lua_State *const l) { - struct LDClient **client; - struct LDUser **user; - if (lua_gettop(l) != 2) { return luaL_error(l, "expecting exactly 2 arguments"); } - client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); - user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); + LDServerSDK *client = (LDServerSDK *) luaL_checkudata(l, 1, "LaunchDarklyClient"); + LDContext *context = (LDContext *) luaL_checkudata(l, 2, "LaunchDarklyContext"); - LDClientIdentify(*client, *user); + LDServerSDK_Identify(*client, *context); return 0; } @@ -1141,23 +1154,26 @@ This does not send analytics events back to LaunchDarkly. static int LuaLDClientAllFlags(lua_State *const l) { - struct LDClient **client; - struct LDUser **user; - struct LDJSON *result; - if (lua_gettop(l) != 2) { return luaL_error(l, "expecting exactly 2 arguments"); } - client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + LDServerSDK *client = (LDServerSDK *) luaL_checkudata(l, 1, "LaunchDarklyClient"); - user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); + LDContext *context = (LDContext *) luaL_checkudata(l, 2, "LaunchDarklyContext"); - result = LDAllFlags(*client, *user); + LDAllFlagsState state = LDServerSDK_AllFlagsState(*client, *context. LD_ALLFLAGSSTATE_DEFAULT); - LuaPushJSON(l, result); - LDJSONFree(result); + char* serialized = LDAllFlagsState_SerializeJSON(state); + + /** TODO: Need to add a C binding to expose this as an LDValue, or have an iterator specific to it. **/ + LDMemory_FreeString(serialized); + + LDAllFlagsState_Free(state); + + /* For now, return an empty table. */ + lua_newtable(l); return 1; } @@ -1183,7 +1199,6 @@ static const struct luaL_Reg launchdarkly_client_methods[] = { { "jsonVariationDetail", LuaLDClientJSONVariationDetail }, { "flush", LuaLDClientFlush }, { "track", LuaLDClientTrack }, - { "alias", LuaLDClientAlias }, { "allFlags", LuaLDClientAllFlags }, { "isInitialized", LuaLDClientIsInitialized }, { "identify", LuaLDClientIdentify }, @@ -1228,7 +1243,7 @@ luaopen_launchdarkly_server_sdk(lua_State *const l) lua_setfield(l, -2, "__index"); ld_luaL_setfuncs(l, launchdarkly_client_methods, 0); - luaL_newmetatable(l, "LaunchDarklyUser"); + luaL_newmetatable(l, "LaunchDarklyContext"); lua_pushvalue(l, -1); lua_setfield(l, -2, "__index"); ld_luaL_setfuncs(l, launchdarkly_user_methods, 0); From f6c41b16442807425f5285d9b245032d3c9311eb Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 13 Dec 2023 11:24:12 -0800 Subject: [PATCH 073/161] launchdarkly-server-sdk.c compiles --- launchdarkly-server-sdk.c | 122 +++++++++++++------------------------- 1 file changed, 41 insertions(+), 81 deletions(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 5bf753e..c79bc2e 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -655,8 +655,9 @@ LuaPushDetails(lua_State *const l, LDEvalDetail details, lua_setfield(l, -2, "reason"); - if (details->hasVariation) { - lua_pushnumber(l, LDEvalDetail_VariationIndex(details)); + size_t out_variation_index; + if (LDEvalDetail_VariationIndex(details, &out_variation_index)) { + lua_pushnumber(l, out_variation_index); lua_setfield(l, -2, "variationIndex"); } @@ -714,18 +715,11 @@ Evaluate a boolean flag and return an explanation static int LuaLDClientBoolVariationDetail(lua_State *const l) { - LDServerSDK *client; - LDContext *context; - - LDEvalDetail details; - if (lua_gettop(l) != 4) { return luaL_error(l, "expecting exactly 4 arguments"); } - LDDetailsInit(&details); - - client = (LDServerSDK *)luaL_checkudata(l, 1, "LaunchDarklyClient"); + LDServerSDK *client = (LDServerSDK *)luaL_checkudata(l, 1, "LaunchDarklyClient"); LDContext *context = (LDContext *) luaL_checkudata(l, 2, "LaunchDarklyContext"); @@ -733,10 +727,11 @@ LuaLDClientBoolVariationDetail(lua_State *const l) const int fallback = lua_toboolean(l, 4); + LDEvalDetail details; const bool result = - LDServerSDK_BoolVariation(*client, *user, key, fallback, &details); + LDServerSDK_BoolVariationDetail(*client, *context, key, fallback, &details); - LuaPushDetails(l, &details, LDValue_NewBool(result)); + LuaPushDetails(l, details, LDValue_NewBool(result)); return 1; } @@ -753,14 +748,11 @@ Evaluate an integer flag static int LuaLDClientIntVariation(lua_State *const l) { - LDServerSDK *client; - LDContext *context; - if (lua_gettop(l) != 4) { return luaL_error(l, "expecting exactly 4 arguments"); } - client = (LDServerSDK *)luaL_checkudata(l, 1, "LaunchDarklyClient"); + LDServerSDK *client = (LDServerSDK *) luaL_checkudata(l, 1, "LaunchDarklyClient"); LDContext *context = (LDContext *) luaL_checkudata(l, 2, "LaunchDarklyContext"); @@ -768,7 +760,7 @@ LuaLDClientIntVariation(lua_State *const l) const int fallback = luaL_checkinteger(l, 4); - const int result = LDServerSDK_IntVariation(*client, *user, key, fallback, NULL); + const int result = LDServerSDK_IntVariation(*client, *context, key, fallback); lua_pushnumber(l, result); @@ -787,17 +779,11 @@ Evaluate an integer flag and return an explanation static int LuaLDClientIntVariationDetail(lua_State *const l) { - LDServerSDK *client; - LDContext *context; - LDEvalDetail details; - if (lua_gettop(l) != 4) { return luaL_error(l, "expecting exactly 4 arguments"); } - LDDetailsInit(&details); - - client = (LDServerSDK *)luaL_checkudata(l, 1, "LaunchDarklyClient"); + LDServerSDK *client = (LDServerSDK *) luaL_checkudata(l, 1, "LaunchDarklyClient"); LDContext *context = (LDContext *) luaL_checkudata(l, 2, "LaunchDarklyContext"); @@ -805,9 +791,10 @@ LuaLDClientIntVariationDetail(lua_State *const l) const int fallback = luaL_checkinteger(l, 4); - const int result = LDIntVariation(*client, *user, key, fallback, &details); + LDEvalDetail details; + const int result = LDServerSDK_IntVariationDetail(*client, *context, key, fallback, &details); - LuaPushDetails(l, &details, LDValue_NewNumber(result)); + LuaPushDetails(l, details, LDValue_NewNumber(result)); return 1; } @@ -824,14 +811,11 @@ Evaluate a double flag static int LuaLDClientDoubleVariation(lua_State *const l) { - LDServerSDK *client; - LDContext *context; - if (lua_gettop(l) != 4) { return luaL_error(l, "expecting exactly 4 arguments"); } - client = (LDServerSDK *)luaL_checkudata(l, 1, "LaunchDarklyClient"); + LDServerSDK *client = (LDServerSDK *) luaL_checkudata(l, 1, "LaunchDarklyClient"); LDContext *context = (LDContext *) luaL_checkudata(l, 2, "LaunchDarklyContext"); @@ -840,7 +824,7 @@ LuaLDClientDoubleVariation(lua_State *const l) const double fallback = lua_tonumber(l, 4); const double result = - LDDoubleVariation(*client, *user, key, fallback, NULL); + LDServerSDK_DoubleVariation(*client, *context, key, fallback); lua_pushnumber(l, result); @@ -859,17 +843,11 @@ Evaluate a double flag and return an explanation static int LuaLDClientDoubleVariationDetail(lua_State *const l) { - LDServerSDK *client; - LDContext *context; - LDEvalDetail details; - if (lua_gettop(l) != 4) { return luaL_error(l, "expecting exactly 4 arguments"); } - LDDetailsInit(&details); - - client = (LDServerSDK *)luaL_checkudata(l, 1, "LaunchDarklyClient"); + LDServerSDK *client = (LDServerSDK *) luaL_checkudata(l, 1, "LaunchDarklyClient"); LDContext *context = (LDContext *) luaL_checkudata(l, 2, "LaunchDarklyContext"); @@ -877,10 +855,11 @@ LuaLDClientDoubleVariationDetail(lua_State *const l) const double fallback = lua_tonumber(l, 4); + LDEvalDetail details; const double result = - LDDoubleVariation(*client, *user, key, fallback, &details); + LDServerSDK_DoubleVariationDetail(*client, *context, key, fallback, &details); - LuaPushDetails(l, &details, LDValue_NewNumber(result)); + LuaPushDetails(l, details, LDValue_NewNumber(result)); return 1; } @@ -897,14 +876,11 @@ Evaluate a string flag static int LuaLDClientStringVariation(lua_State *const l) { - LDServerSDK *client; - LDContext *context; - if (lua_gettop(l) != 4) { return luaL_error(l, "expecting exactly 4 arguments"); } - client = (LDServerSDK *)luaL_checkudata(l, 1, "LaunchDarklyClient"); + LDServerSDK *client = (LDServerSDK *)luaL_checkudata(l, 1, "LaunchDarklyClient"); LDContext *context = (LDContext *) luaL_checkudata(l, 2, "LaunchDarklyContext"); @@ -912,12 +888,12 @@ LuaLDClientStringVariation(lua_State *const l) const char *const fallback = luaL_checkstring(l, 4); - char *const result = - LDStringVariation(*client, *user, key, fallback, NULL); + char *result = + LDServerSDK_StringVariation(*client, *context, key, fallback); lua_pushstring(l, result); - LDFree(result); + LDMemory_FreeString(result); return 1; } @@ -934,17 +910,12 @@ Evaluate a string flag and return an explanation static int LuaLDClientStringVariationDetail(lua_State *const l) { - LDServerSDK *client; - LDContext *context; - LDEvalDetail details; if (lua_gettop(l) != 4) { return luaL_error(l, "expecting exactly 4 arguments"); } - LDDetailsInit(&details); - - client = (LDServerSDK *)luaL_checkudata(l, 1, "LaunchDarklyClient"); + LDServerSDK *client = (LDServerSDK *)luaL_checkudata(l, 1, "LaunchDarklyClient"); LDContext *context = (LDContext *) luaL_checkudata(l, 2, "LaunchDarklyContext"); @@ -952,12 +923,13 @@ LuaLDClientStringVariationDetail(lua_State *const l) const char *const fallback = luaL_checkstring(l, 4); - char *const result = - LDStringVariation(*client, *user, key, fallback, &details); + LDEvalDetail details; + char *result = + LDServerSDK_StringVariationDetail(*client, *context, key, fallback, &details); - LuaPushDetails(l, &details, LDValue_NewString(result)); + LuaPushDetails(l, details, LDValue_NewString(result)); - LDFree(result); + LDMemory_FreeString(result); return 1; } @@ -974,28 +946,24 @@ Evaluate a json flag static int LuaLDClientJSONVariation(lua_State *const l) { - LDServerSDK *client; - LDContext *context; - struct LDJSON *fallback, *result; - if (lua_gettop(l) != 4) { return luaL_error(l, "expecting exactly 4 arguments"); } - client = (LDServerSDK *)luaL_checkudata(l, 1, "LaunchDarklyClient"); + LDServerSDK *client = (LDServerSDK *) luaL_checkudata(l, 1, "LaunchDarklyClient"); LDContext *context = (LDContext *) luaL_checkudata(l, 2, "LaunchDarklyContext"); const char *const key = luaL_checkstring(l, 3); - fallback = LuaValueToJSON(l, 4); + LDValue fallback = LuaValueToJSON(l, 4); - result = LDServerSDK_JSONVariation(*client, *user, key, fallback, NULL); + LDValue result = LDServerSDK_JsonVariation(*client, *context, key, fallback); LuaPushJSON(l, result); - LDJSONFree(fallback); - LDJSONFree(result); + LDValue_Free(fallback); + LDValue_Free(result); return 1; } @@ -1012,25 +980,20 @@ Evaluate a json flag and return an explanation static int LuaLDClientJSONVariationDetail(lua_State *const l) { - LDServerSDK *client; - LDContext *context; - LDValue fallback, result; - - LDEvalDetail details; - if (lua_gettop(l) != 4) { return luaL_error(l, "expecting exactly 4 arguments"); } - client = (LDServerSDK *)luaL_checkudata(l, 1, "LaunchDarklyClient"); + LDServerSDK *client = (LDServerSDK *) luaL_checkudata(l, 1, "LaunchDarklyClient"); - context = (LDContext *)luaL_checkudata(l, 2, "LaunchDarklyContext"); + LDContext *context = (LDContext *) luaL_checkudata(l, 2, "LaunchDarklyContext"); const char *const key = luaL_checkstring(l, 3); - fallback = LuaValueToJSON(l, 4); + LDValue fallback = LuaValueToJSON(l, 4); - result = LDServerSDK_JSONVariationDetail(*client, *context, key, fallback, &details); + LDEvalDetail details; + LDValue result = LDServerSDK_JsonVariationDetail(*client, *context, key, fallback, &details); LuaPushDetails(l, details, result); @@ -1110,8 +1073,6 @@ initialization timeout was reached. static int LuaLDClientIsInitialized(lua_State *const l) { - struct LDClient **client; - if (lua_gettop(l) != 1) { return luaL_error(l, "expecting exactly 1 argument"); } @@ -1162,8 +1123,7 @@ LuaLDClientAllFlags(lua_State *const l) LDContext *context = (LDContext *) luaL_checkudata(l, 2, "LaunchDarklyContext"); - LDAllFlagsState state = LDServerSDK_AllFlagsState(*client, *context. LD_ALLFLAGSSTATE_DEFAULT); - + LDAllFlagsState state = LDServerSDK_AllFlagsState(*client, *context, LD_ALLFLAGSSTATE_DEFAULT); char* serialized = LDAllFlagsState_SerializeJSON(state); @@ -1253,7 +1213,7 @@ luaopen_launchdarkly_server_sdk(lua_State *const l) lua_setfield(l, -2, "__index"); ld_luaL_setfuncs(l, launchdarkly_store_methods, 0); - #if LUA_VERSION_NUM == 503 || LUA_VERSION_NUM == 502 + #if LUA_VERSION_NUM >= 502 luaL_newlib(l, launchdarkly_functions); #else luaL_register(l, "launchdarkly-server-sdk", launchdarkly_functions); From 9942fef2299bd72613aab683130eaacb586b998c Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 13 Dec 2023 13:00:25 -0800 Subject: [PATCH 074/161] update unit tests --- launchdarkly-server-sdk.c | 100 ++++++++++++++++++++++++++++++++------ test.lua | 65 +++++++++++++------------ 2 files changed, 119 insertions(+), 46 deletions(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index c79bc2e..cb7fe39 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -646,6 +646,72 @@ LuaLDClientClose(lua_State *const l) return 0; } +static void LuaPushReason(lua_State *const l, LDEvalReason reason) { + enum LDEvalReason_Kind reason_kind = LDEvalReason_Kind(reason); + + // Push a string representation opf each reason + switch (reason_kind) { + case LD_EVALREASON_OFF: + lua_pushstring(l, "OFF"); + break; + case LD_EVALREASON_FALLTHROUGH: + lua_pushstring(l, "FALLTHROUGH"); + break; + case LD_EVALREASON_TARGET_MATCH: + lua_pushstring(l, "TARGET_MATCH"); + break; + case LD_EVALREASON_RULE_MATCH: + lua_pushstring(l, "RULE_MATCH"); + break; + case LD_EVALREASON_PREREQUISITE_FAILED: + lua_pushstring(l, "PREREQUISITE_FAILED"); + break; + case LD_EVALREASON_ERROR: + lua_pushstring(l, "ERROR"); + break; + default: + lua_pushstring(l, "UNKNOWN"); + break; + } + + lua_setfield(l, -2, "kind"); + + enum LDEvalReason_ErrorKind out_error_kind; + if (LDEvalReason_ErrorKind(reason, &out_error_kind)) { + + // Switch on out_error_kind, and push the string representation. + switch (out_error_kind) { + case LD_EVALREASON_ERROR_CLIENT_NOT_READY: + lua_pushstring(l, "CLIENT_NOT_READY"); + break; + case LD_EVALREASON_ERROR_USER_NOT_SPECIFIED: + lua_pushstring(l, "USER_NOT_SPECIFIED"); + break; + case LD_EVALREASON_ERROR_FLAG_NOT_FOUND: + lua_pushstring(l, "FLAG_NOT_FOUND"); + break; + case LD_EVALREASON_ERROR_WRONG_TYPE: + lua_pushstring(l, "WRONG_TYPE"); + break; + case LD_EVALREASON_ERROR_MALFORMED_FLAG: + lua_pushstring(l, "MALFORMED_FLAG"); + break; + case LD_EVALREASON_ERROR_EXCEPTION: + lua_pushstring(l, "EXCEPTION"); + break; + default: + lua_pushstring(l, "UNKNOWN"); + break; + } + + lua_setfield(l, -2, "errorKind"); + } + + bool in_experiment = LDEvalReason_InExperiment(reason); + lua_pushboolean(l, in_experiment); + lua_setfield(l, -2, "inExperiment"); +} + static void LuaPushDetails(lua_State *const l, LDEvalDetail details, LDValue value) @@ -653,7 +719,13 @@ LuaPushDetails(lua_State *const l, LDEvalDetail details, lua_newtable(l); /** TODO: C bindings for this? */ - lua_setfield(l, -2, "reason"); + + LDEvalReason out_reason; + if (LDEvalDetail_Reason(details, &out_reason)) { + lua_newtable(l); + LuaPushReason(l, out_reason); + lua_setfield(l, -2, "reason"); + } size_t out_variation_index; if (LDEvalDetail_VariationIndex(details, &out_variation_index)) { @@ -672,7 +744,7 @@ LuaPushDetails(lua_State *const l, LDEvalDetail details, Evaluate a boolean flag @class function @name boolVariation -@tparam user user An opaque user object from @{makeUser} +@tparam context context An opaque context object from @{makeUser} or @{makeContext} @tparam string key The key of the flag to evaluate. @tparam boolean fallback The value to return on error @treturn boolean The evaluation result, or the fallback value @@ -707,7 +779,7 @@ LuaLDClientBoolVariation(lua_State *const l) Evaluate a boolean flag and return an explanation @class function @name boolVariationDetail -@tparam user user An opaque user object from @{makeUser} +@tparam context context An opaque context object from @{makeUser} or @{makeContext} @tparam string key The key of the flag to evaluate. @tparam boolean fallback The value to return on error @treturn table The evaluation explanation @@ -740,7 +812,7 @@ LuaLDClientBoolVariationDetail(lua_State *const l) Evaluate an integer flag @class function @name intVariation -@tparam user user An opaque user object from @{makeUser} +@tparam context context An opaque context object from @{makeUser} or @{makeContext} @tparam string key The key of the flag to evaluate. @tparam int fallback The value to return on error @treturn int The evaluation result, or the fallback value @@ -771,7 +843,7 @@ LuaLDClientIntVariation(lua_State *const l) Evaluate an integer flag and return an explanation @class function @name intVariationDetail -@tparam user user An opaque user object from @{makeUser} +@tparam context context An opaque context object from @{makeUser} or @{makeContext} @tparam string key The key of the flag to evaluate. @tparam int fallback The value to return on error @treturn table The evaluation explanation @@ -803,7 +875,7 @@ LuaLDClientIntVariationDetail(lua_State *const l) Evaluate a double flag @class function @name doubleVariation -@tparam user user An opaque user object from @{makeUser} +@tparam context context An opaque context object from @{makeUser} or @{makeContext} @tparam string key The key of the flag to evaluate. @tparam number fallback The value to return on error @treturn double The evaluation result, or the fallback value @@ -835,7 +907,7 @@ LuaLDClientDoubleVariation(lua_State *const l) Evaluate a double flag and return an explanation @class function @name doubleVariationDetail -@tparam user user An opaque user object from @{makeUser} +@tparam context context An opaque context object from @{makeUser} or @{makeContext} @tparam string key The key of the flag to evaluate. @tparam number fallback The value to return on error @treturn table The evaluation explanation @@ -868,7 +940,7 @@ LuaLDClientDoubleVariationDetail(lua_State *const l) Evaluate a string flag @class function @name stringVariation -@tparam user user An opaque user object from @{makeUser} +@tparam context context An opaque context object from @{makeUser} or @{makeContext} @tparam string key The key of the flag to evaluate. @tparam string fallback The value to return on error @treturn string The evaluation result, or the fallback value @@ -902,7 +974,7 @@ LuaLDClientStringVariation(lua_State *const l) Evaluate a string flag and return an explanation @class function @name stringVariationDetail -@tparam user user An opaque user object from @{makeUser} +@tparam context context An opaque context object from @{makeUser} or @{makeContext} @tparam string key The key of the flag to evaluate. @tparam string fallback The value to return on error @treturn table The evaluation explanation @@ -938,7 +1010,7 @@ LuaLDClientStringVariationDetail(lua_State *const l) Evaluate a json flag @class function @name jsonVariation -@tparam user user An opaque user object from @{makeUser} +@tparam context context An opaque context object from @{makeUser} or @{makeContext} @tparam string key The key of the flag to evaluate. @tparam table fallback The value to return on error @treturn table The evaluation result, or the fallback value @@ -972,7 +1044,7 @@ LuaLDClientJSONVariation(lua_State *const l) Evaluate a json flag and return an explanation @class function @name jsonVariationDetail -@tparam user user An opaque user object from @{makeUser} +@tparam context context An opaque context object from @{makeUser} or @{makeContext} @tparam string key The key of the flag to evaluate. @tparam table fallback The value to return on error @treturn table The evaluation explanation @@ -1026,7 +1098,7 @@ Reports that a user has performed an event. Custom data, and a metric can be attached to the event as JSON. @function track @tparam string key The name of the event -@tparam user user An opaque user object from @{makeUser} +@tparam context context An opaque context object from @{makeUser} or @{makeContext} @tparam[opt] table data A value to be associated with an event @tparam[optchain] number metric A value to be associated with an event @treturn nil @@ -1087,7 +1159,7 @@ LuaLDClientIsInitialized(lua_State *const l) /*** Generates an identify event for a user. @function identify -@tparam user user An opaque user object from @{makeUser} +@tparam context context An opaque context object from @{makeUser} or @{makeContext} @treturn nil */ static int @@ -1109,7 +1181,7 @@ LuaLDClientIdentify(lua_State *const l) Returns a map from feature flag keys to values for a given user. This does not send analytics events back to LaunchDarkly. @function allFlags -@tparam user user An opaque user object from @{makeUser} +@tparam context context An opaque context object from @{makeUser} or @{makeContext} @treturn table */ static int diff --git a/test.lua b/test.lua index f6344f0..833d4d5 100644 --- a/test.lua +++ b/test.lua @@ -1,6 +1,6 @@ local u = require('luaunit') local l = require("launchdarkly_server_sdk") -local r = require("launchdarkly_server_sdk_redis") +-- local r = require("launchdarkly_server_sdk_redis") function logger(level, line) print(level .. ": " .. line) @@ -27,7 +27,7 @@ end function TestAll:testBoolVariation() local e = false - u.assertEquals(e, makeTestClient():boolVariation(user, "test", e)) + u.assertEquals(makeTestClient():boolVariation(user, "test", e), e) end function TestAll:testBoolVariationDetail() @@ -35,15 +35,16 @@ function TestAll:testBoolVariationDetail() value = true, reason = { kind = "ERROR", - errorKind = "CLIENT_NOT_READY" + errorKind = "FLAG_NOT_FOUND", + inExperiment = false } } - u.assertEquals(e, makeTestClient():boolVariationDetail(user, "test", true)) + u.assertEquals(makeTestClient():boolVariationDetail(user, "test", true), e) end function TestAll:testIntVariation() local e = 3 - u.assertEquals(e, makeTestClient():intVariation(user, "test", e)) + u.assertEquals(makeTestClient():intVariation(user, "test", e), e) end function TestAll:testIntVariationDetail() @@ -51,15 +52,16 @@ function TestAll:testIntVariationDetail() value = 5, reason = { kind = "ERROR", - errorKind = "CLIENT_NOT_READY" + errorKind = "FLAG_NOT_FOUND", + inExperiment = false } } - u.assertEquals(e, makeTestClient():intVariationDetail(user, "test", 5)) + u.assertEquals(makeTestClient():intVariationDetail(user, "test", 5), e) end function TestAll:testDoubleVariation() - local e = 5.3 - u.assertEquals(e, makeTestClient():doubleVariation(user, "test", e)) + local e = 12.5 + u.assertEquals(makeTestClient():doubleVariation(user, "test", e), e) end function TestAll:testDoubleVariationDetail() @@ -67,15 +69,16 @@ function TestAll:testDoubleVariationDetail() value = 6.2, reason = { kind = "ERROR", - errorKind = "CLIENT_NOT_READY" + errorKind = "FLAG_NOT_FOUND", + inExperiment = false } } - u.assertEquals(e, makeTestClient():doubleVariationDetail(user, "test", 6.2)) + u.assertEquals( makeTestClient():doubleVariationDetail(user, "test", 6.2), e) end function TestAll:testStringVariation() local e = "a" - u.assertEquals(e, makeTestClient():stringVariation(user, "test", e)) + u.assertEquals(makeTestClient():stringVariation(user, "test", e), e) end function TestAll:testStringVariationDetail() @@ -83,15 +86,16 @@ function TestAll:testStringVariationDetail() value = "f", reason = { kind = "ERROR", - errorKind = "CLIENT_NOT_READY" + errorKind = "FLAG_NOT_FOUND", + inExperiment = false } } - u.assertEquals(e, makeTestClient():stringVariationDetail(user, "test", "f")) + u.assertEquals(makeTestClient():stringVariationDetail(user, "test", "f"), e) end function TestAll:testJSONVariation() local e = { ["a"] = "b" } - u.assertEquals(e, makeTestClient():jsonVariation(user, "test", e)) + u.assertEquals(makeTestClient():jsonVariation(user, "test", e), e) end function TestAll:testJSONVariationDetail() @@ -99,31 +103,28 @@ function TestAll:testJSONVariationDetail() value = { a = "b" }, reason = { kind = "ERROR", - errorKind = "CLIENT_NOT_READY" + errorKind = "FLAG_NOT_FOUND", + inExperiment = false } } - u.assertEquals(e, makeTestClient():jsonVariationDetail(user, "test", { a = "b" })) + u.assertEquals(makeTestClient():jsonVariationDetail(user, "test", { a = "b" }), e) end function TestAll:testIdentify() makeTestClient():identify(user) end -function TestAll:testAlias() - makeTestClient():alias(user, l.makeUser({ key = "bob" })) -end - -function TestAll:testRedisBasic() - local c = l.clientInit({ - key = "sdk-test", - featureStoreBackend = r.makeStore({}), - offline = true - }, 0) - - local e = false - - u.assertEquals(e, c:boolVariation(user, "test", e)) -end +--function TestAll:testRedisBasic() +-- local c = l.clientInit({ +-- key = "sdk-test", +-- featureStoreBackend = r.makeStore({}), +-- offline = true +-- }, 0) +-- +-- local e = false +-- +-- u.assertEquals(e, c:boolVariation(user, "test", e)) +--end function TestAll:testVersion() local version = l.version() From 0f1a6f8cef0c9314b56648192cd60bc5dc461f19 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 13 Dec 2023 16:30:51 -0800 Subject: [PATCH 075/161] add logging callbacks --- launchdarkly-server-sdk.c | 57 ++++++++++++++++++++++++++------------- test.lua | 22 ++++++++------- 2 files changed, 52 insertions(+), 27 deletions(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index cb7fe39..c0cfe55 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -33,7 +33,9 @@ LuaArrayToJSON(lua_State *const l, const int i); static void LuaPushJSON(lua_State *const l, LDValue j); -static int globalLoggingCallback; +static int globalLogEnabledCallback; +static int globalLogWriteCallback; + static lua_State *globalLuaState; static int lua_tablelen(lua_State *L, int index) @@ -45,26 +47,36 @@ static int lua_tablelen(lua_State *L, int index) #endif } -static void -logHandler(const enum LDLogLevel level, const char * const line) -{ - lua_rawgeti(globalLuaState, LUA_REGISTRYINDEX, globalLoggingCallback); + +bool logEnabled(enum LDLogLevel level, void *user_data /* ignored */) { + lua_rawgeti(globalLuaState, LUA_REGISTRYINDEX, globalLogEnabledCallback); + + lua_pushstring(globalLuaState, LDLogLevel_Name(level, "unknown")); + + lua_call(globalLuaState, 1, 1); // one argument (level, string), one result (enable, boolean). + + return lua_toboolean(globalLuaState, -1); +} + +void logWrite(enum LDLogLevel level, const char* msg, void *user_data /* ignored */) { + lua_rawgeti(globalLuaState, LUA_REGISTRYINDEX, globalLogWriteCallback); lua_pushstring(globalLuaState, LDLogLevel_Name(level, "unknown")); - lua_pushstring(globalLuaState, line); + lua_pushstring(globalLuaState, msg); - lua_call(globalLuaState, 2, 0); + lua_call(globalLuaState, 2, 0); // two args (level, string + msg, string), no results. } + /*** +TODO: Document that the log levels have changed and there is an additional callback, and removed the log level arg. Set the global logger for all SDK operations. This function is not thread -safe, and if used should be done so before other operations. The default -log level is "INFO". +safe, and if used should be done so before other operations. @function registerLogger -@tparam string logLevel The level to at. Available options are: -"FATAL", "CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "TRACE". -@tparam function cb The logging handler. Callback must be of the form +@tparam function writeCb The logging write handler. Callback must be of the form "function (logLevel, logLine) ... end". +@tparam function enabledCb The log level enabled handler. Callback must be of the form +"function (logLevel) -> bool ... end". Return true if the given log level is enabled. */ static int LuaLDRegisterLogger(lua_State *const l) @@ -73,13 +85,9 @@ LuaLDRegisterLogger(lua_State *const l) return luaL_error(l, "expecting exactly 2 arguments"); } - const char *const level = luaL_checkstring(l, 1); - globalLuaState = l; - globalLoggingCallback = luaL_ref(l, LUA_REGISTRYINDEX); - - // TODO: Install custom loggger - // LDConfigureGlobalLogger(LDLogLevel_Enum(level, LD_LOG_INFO), logHandler); + globalLogEnabledCallback = luaL_ref(l, LUA_REGISTRYINDEX); + globalLogWriteCallback = luaL_ref(l, LUA_REGISTRYINDEX); return 0; } @@ -536,6 +544,19 @@ makeConfig(lua_State *const l, const int i) } } + + if (globalLogEnabledCallback != LUA_NOREF && globalLogWriteCallback != LUA_NOREF) { + struct LDLogBackend backend; + LDLogBackend_Init(&backend); + + backend.Write = logWrite; + backend.Enabled = logEnabled; + + LDLoggingCustomBuilder custom_logging = LDLoggingCustomBuilder_New(); + LDLoggingCustomBuilder_Backend(custom_logging, backend); + LDServerConfigBuilder_Logging_Custom(builder, custom_logging); + } + LDServerConfigBuilder_HttpProperties_WrapperName(builder, "lua-server-sdk"); LDServerConfigBuilder_HttpProperties_WrapperVersion(builder, SDKVersion); diff --git a/test.lua b/test.lua index 833d4d5..e85dc96 100644 --- a/test.lua +++ b/test.lua @@ -2,18 +2,22 @@ local u = require('luaunit') local l = require("launchdarkly_server_sdk") -- local r = require("launchdarkly_server_sdk_redis") -function logger(level, line) - print(level .. ": " .. line) +function logWrite(level, line) + print("[" .. level .. "]" .. " " .. line) end -l.registerLogger("TRACE", logger) +function logEnabled(level) + return level == "warn" or level == "error" +end + +l.registerLogger(logWrite, logEnabled) TestAll = {} function makeTestClient() local c = l.clientInit({ key = "sdk-test", - offline = true + offline = True }, 0) return c @@ -35,7 +39,7 @@ function TestAll:testBoolVariationDetail() value = true, reason = { kind = "ERROR", - errorKind = "FLAG_NOT_FOUND", + errorKind = "CLIENT_NOT_READY", inExperiment = false } } @@ -52,7 +56,7 @@ function TestAll:testIntVariationDetail() value = 5, reason = { kind = "ERROR", - errorKind = "FLAG_NOT_FOUND", + errorKind = "CLIENT_NOT_READY", inExperiment = false } } @@ -69,7 +73,7 @@ function TestAll:testDoubleVariationDetail() value = 6.2, reason = { kind = "ERROR", - errorKind = "FLAG_NOT_FOUND", + errorKind = "CLIENT_NOT_READY", inExperiment = false } } @@ -86,7 +90,7 @@ function TestAll:testStringVariationDetail() value = "f", reason = { kind = "ERROR", - errorKind = "FLAG_NOT_FOUND", + errorKind = "CLIENT_NOT_READY", inExperiment = false } } @@ -103,7 +107,7 @@ function TestAll:testJSONVariationDetail() value = { a = "b" }, reason = { kind = "ERROR", - errorKind = "FLAG_NOT_FOUND", + errorKind = "CLIENT_NOT_READY", inExperiment = false } } From bf2ffba990502c3623a08313d71ffa3c4907e5fb Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 18 Dec 2023 16:55:05 -0800 Subject: [PATCH 076/161] update redis binding --- launchdarkly-server-sdk-redis.c | 55 +++++----------- launchdarkly-server-sdk.c | 112 ++++++++++++++++++++++++++++---- test.lua | 11 +++- 3 files changed, 127 insertions(+), 51 deletions(-) diff --git a/launchdarkly-server-sdk-redis.c b/launchdarkly-server-sdk-redis.c index d6282e9..33625de 100644 --- a/launchdarkly-server-sdk-redis.c +++ b/launchdarkly-server-sdk-redis.c @@ -12,52 +12,33 @@ Server-side SDK for LaunchDarkly Redis store. #include #include + + /*** -Initialize a store backend -@function makeStore -@tparam table fields list of configuration options -@tparam[opt] string fields.host Hostname for Redis. -@tparam[opt] int fields.port Port for Redis. -@tparam[opt] string fields.prefix Redis key prefix for SDK values. -@tparam[opt] int fields.poolSize Number of Redis connections to maintain. -@return A fresh Redis store backend. +Create a Redis data source, which can be used instead +of a LaunchDarkly Streaming or Polling data source. +@function makeRedisSource +@tparam string uri Redis URI. +@tparam string prefix Prefix to use when reading SDK data from Redis. +@return A new Redis data source, suitable for configuration in the SDK's data system. */ static int -LuaLDRedisMakeStore(lua_State *const l) +LuaLDRedisMakeSource(lua_State *const l) { - struct LDRedisConfig *config; - struct LDStoreInterface *storeInterface; - if (lua_gettop(l) != 1) { - return luaL_error(l, "expecting exactly 1 argument"); + return luaL_error(l, "expecting exactly 2 arguments"); } - luaL_checktype(l, 1, LUA_TTABLE); - - config = LDRedisConfigNew(); - - lua_getfield(l, 1, "host"); - - if (lua_isstring(l, -1)) { - LDRedisConfigSetHost(config, luaL_checkstring(l, -1)); - } - - lua_getfield(l, 1, "prefix"); - - if (lua_isstring(l, -1)) { - LDRedisConfigSetPrefix(config, luaL_checkstring(l, -1)); - } - - lua_getfield(l, 1, "port"); - - if (lua_isnumber(l, -1)) { - LDRedisConfigSetPort(config, luaL_checkinteger(l, -1)); - } - lua_getfield(l, 1, "poolSize"); + const char* uri = luaL_checkstring(l, 1); + const char* prefix = luaL_checkstring(l, 2); - if (lua_isnumber(l, -1)) { - LDRedisConfigSetPoolSize(config, luaL_checkinteger(l, -1)); + LDServerSDK_RedisSource out_source; + LDStatus status = LDServerSDK_RedisSource_Create(uri, prefix, &out_source); + if (!LDStatus_Ok(status)) { + lua_pushstring(l, LDStatus_Error(status)); + LDStatus_Free(status); + return luaL_error(l, "failed to create Redis source: " .. lua_tostring(l, -1)); } storeInterface = LDStoreInterfaceRedisNew(config); diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index c0cfe55..57bb53d 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -246,7 +246,7 @@ LuaPushJSON(lua_State *const l, LDValue j) } /*** -Create a new opaque user object. +Create a new opaque context object of kind 'user'. @function makeUser @tparam table fields list of user fields. @tparam string fields.key The user's key @@ -258,11 +258,10 @@ Create a new opaque user object. @tparam[opt] string fields.name Set the user's name @tparam[opt] string fields.avatar Set the user's avatar @tparam[opt] string fields.country Set the user's country -@tparam[opt] string fields.secondary Set the user's secondary key @tparam[opt] table fields.privateAttributeNames A list of attributes to redact -@tparam[opt] table fields.custom Set the user's custom JSON -@return an opaque user object +@tparam[opt] table fields.custom A table of attributes to set on the user. +@return an opaque context object */ static int LuaLDUserNew(lua_State *const l) @@ -330,13 +329,9 @@ LuaLDUserNew(lua_State *const l) } - lua_getfield(l, 1, "secondary"); - - if (lua_isstring(l, -1)) { - // TODO: Log a warning that this was removed + // TODO: Document that secondary was removed - } lua_getfield(l, 1, "custom"); @@ -416,6 +411,17 @@ LuaLDUserFree(lua_State *const l) return 0; } +// makeConfig({ +// dataSystem: { +// enabled: bool +// method: streaming +// +// +// } +// }) + + + static LDServerConfig makeConfig(lua_State *const l, const int i) { @@ -447,11 +453,91 @@ makeConfig(lua_State *const l, const int i) LDServerConfigBuilder_ServiceEndpoints_EventsBaseURL(builder, luaL_checkstring(l, -1)); } - lua_getfield(l, i, "stream"); +/* + local user = l.makeUser({ + key = "alice", + dataSystem = { + enabled = true, + backgroundSync = { + source = "launchdarkly_streaming", + initialReconnectDelayMs = 1000 + } +lazyLoad = { + source = makeRedisSource("redis://localhost:6379", "prefix") +} + } + }) +*/ + +// TODO: stream, useLDD, and pollInterval, and featureStoreBackend all unified under new dataSystem key + lua_getfield(l, i, "dataSystem"); - if (lua_isboolean(l, -1)) { - // TODO: Switch between poll/streaming data systems - //LDConfigSetStream(config, lua_toboolean(l, -1)); + if (lua_istable(l, -1)) { + lua_getfield(l, i, "enabled"); + if (lua_isboolean(l, -1)) { + LDServerConfigBuilder_DataSystem_Enabled(builder, lua_toboolean(l, -1)); + } + + lua_getfield(l, i, "backgroundSync"); + if (lua_isnil(l, -1)) { + lua_pop(l, 1); /* not an error, perhaps lazyLoad was configured instead */ + + lua_getfield(l, i, "lazyLoad"); + if (lua_isnil(l, -1)) { + return luaL_error(l, "dataSystem.lazyLoad specified, must specify a source"); + } else if (lua_istable(l, -1)) { + lua_getfield(l, i, "source"); + + if (lua_isnil(l, -1)) { + return luaL_error(l, "dataSystem.lazyLoad.source must be provided"); + } + + LDServerLazyLoadSource *source = luaL_checkudata(l, 1, "LaunchDarklyStoreInterface"); + + LDServerLazyLoadBuilder lazy_load_builder = LDServerLazyLoadBuilder_New(); + + LDServerLazyLoadBuilder_Source(lazy_load_builder, source); + LDServerLazyLoadBuilder_CacheRefreshMs(lazy_load_builder, 30000); + LDServerLazyLoadBuilder_CacheEviction(lazy_load_builder, LDServerLazyLoadCacheEviction_Disabled); + + + /* This should be a metatable containing the redis source */ + + } else { + return luaL_error(l, "dataSystem.lazyLoad must be a table"); + } + } else if (lua_istable(l, -1)) { + lua_getfield(l, i, "source"); + + if (lua_isstring(l, -1)) { + const char *const source = luaL_checkstring(l, -1); + if (strcmp(source, "launchdarkly_streaming") == 0) { + LDServerDataSourceStreamBuilder stream_builder = LDServerDataSourceStreamBuilder_New(); + + lua_getfield(l, i, "initialReconnectDelayMs"); + if (lua_isnumber(l, -1)) { + LDServerDataSourceStreamBuilder_InitialReconnectDelayMs(stream_builder, luaL_checkinteger(l, -1)); + } + + LDServerConfigBuilder_DataSystem_BackgroundSync_Streaming(builder, stream_builder); + } else if (strcmp(source, "launchdarkly_polling") == 0) { + LDServerDataSourcePollBuilder poll_builder = LDServerDataSourcePollBuilder_New(); + + lua_getfield(l, i, "intervalSeconds"); + if (lua_isnumber(l, -1)) { + LDServerDataSourcePollBuilder_IntervalS(poll_builder, luaL_checkinteger(l, -1)); + } + + LDServerConfigBuilder_DataSystem_BackgroundSync_Polling(builder, poll_builder); + } else { + return luaL_error(l, "dataSystem.method must be 'streaming' or 'polling'"); + } + } else { + return luaL_error(l, "dataSystem.backgroundSync.source must be a string"); + } + } else { + return luaL_error(l, "dataSystem.backgroundSync must be a table"); + } } lua_getfield(l, i, "sendEvents"); diff --git a/test.lua b/test.lua index e85dc96..4fa0a57 100644 --- a/test.lua +++ b/test.lua @@ -23,7 +23,16 @@ function makeTestClient() return c end -local user = l.makeUser({ key = "alice" }) +local user = l.makeUser({ + key = "alice", + dataSystem = { + enabled = true, + method = "streaming", + params = { + initialReconnectDelayMs = 1000 + } + } +}) function TestAll:tearDown() collectgarbage("collect") From 5766002c7677b360af5497d68fca75c98291ca3a Mon Sep 17 00:00:00 2001 From: Sarah Day Date: Tue, 15 Feb 2022 13:07:32 -0800 Subject: [PATCH 077/161] Adds singleton guidance to Lua docs --- launchdarkly-server-sdk.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 57bb53d..63a95b6 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -655,7 +655,7 @@ lazyLoad = { } /*** -Initialize a new client, and connect to LaunchDarkly. +Initialize a new client, and connect to LaunchDarkly. Applications should instantiate a single instance for the lifetime of their application. @function makeClient @tparam table config list of configuration options @tparam string config.key Environment SDK key From ee9fcf489300cc1371d9484e1c9bcf51eacb31a0 Mon Sep 17 00:00:00 2001 From: "ld-repository-standards[bot]" <113625520+ld-repository-standards[bot]@users.noreply.github.com> Date: Mon, 26 Jun 2023 22:20:55 +0000 Subject: [PATCH 078/161] Add file SECURITY.md --- SECURITY.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..065a880 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,5 @@ +#Reporting and Fixing Security Issues + +Please report all security issues to the LaunchDarkly security team by submitting a bug bounty report to our [HackerOne program](https://hackerone.com/launchdarkly?type=team). LaunchDarkly will triage and address all valid security issues following the response targets defined in our program policy. Valid security issues may be eligible for a bounty. + +Please do not open issues or pull requests for security issues. This makes the problem immediately visible to everyone, including potentially malicious actors. From 4bebbdfdc97d235f580b61607d84b0497a4a740c Mon Sep 17 00:00:00 2001 From: "ld-repository-standards[bot]" <113625520+ld-repository-standards[bot]@users.noreply.github.com> Date: Fri, 7 Jul 2023 22:35:48 +0000 Subject: [PATCH 079/161] Add file SECURITY.md --- SECURITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SECURITY.md b/SECURITY.md index 065a880..10f1d1a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,4 +1,4 @@ -#Reporting and Fixing Security Issues +# Reporting and Fixing Security Issues Please report all security issues to the LaunchDarkly security team by submitting a bug bounty report to our [HackerOne program](https://hackerone.com/launchdarkly?type=team). LaunchDarkly will triage and address all valid security issues following the response targets defined in our program policy. Valid security issues may be eligible for a bounty. From 1722755f20d50eb5e819f7a44d17298c572df53b Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 18 Dec 2023 14:11:38 -0800 Subject: [PATCH 080/161] ci: replace Releaser/circleCI with release-please/Github Actions (#29) This PR synchronizes the internal repo with public. It replaces CircleCI with Github Actions, and releaser configuration with `release-please`. --------- Co-authored-by: hroederld Co-authored-by: Ben Woskow <48036130+bwoskow-ld@users.noreply.github.com> Co-authored-by: LaunchDarklyCI Co-authored-by: ember-stevens <79482775+ember-stevens@users.noreply.github.com> Co-authored-by: LaunchDarklyReleaseBot Co-authored-by: Ember Stevens Co-authored-by: ld-repository-standards[bot] <113625520+ld-repository-standards[bot]@users.noreply.github.com> Co-authored-by: Kane Parkinson <93555788+kparkinson-ld@users.noreply.github.com> Co-authored-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Co-authored-by: Sarah Day --- scripts/config.yml | 16 ---------------- scripts/prepare.sh | 7 ------- scripts/update-version.sh | 5 ----- 3 files changed, 28 deletions(-) delete mode 100644 scripts/config.yml delete mode 100755 scripts/prepare.sh delete mode 100755 scripts/update-version.sh diff --git a/scripts/config.yml b/scripts/config.yml deleted file mode 100644 index 74f2fbe..0000000 --- a/scripts/config.yml +++ /dev/null @@ -1,16 +0,0 @@ -version: 2 - -repo: - public: lua-server-sdk - private: lua-server-sdk-private - -jobs: - - docker: - image: ubuntu:18.04 - -documentation: - title: LaunchDarkly Server-Side SDK for Lua - gitHubPages: true - -sdk: - displayName: "Lua (server-side)" diff --git a/scripts/prepare.sh b/scripts/prepare.sh deleted file mode 100755 index 1df986f..0000000 --- a/scripts/prepare.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -set -e - -apt-get update -y && apt-get install -y luajit lua-ldoc zip ca-certificates \ - curl libpcre3 libcurl4-openssl-dev cmake libhiredis-dev git \ - build-essential libpcre3-dev diff --git a/scripts/update-version.sh b/scripts/update-version.sh deleted file mode 100755 index 02920ed..0000000 --- a/scripts/update-version.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -set -e - -sed -i "s/#define SDKVersion .*/#define SDKVersion \"${LD_RELEASE_VERSION}\"/" 'launchdarkly-server-sdk.c' From ca21a6c4dcf6b5fdc6106ecc3b2bc5e9a9f38c1f Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 18 Dec 2023 15:16:02 -0800 Subject: [PATCH 081/161] install boost on runner --- .github/actions/ci/action.yml | 10 ++++--- .github/actions/install-boost/action.yml | 26 +++++++++++++++++++ ...{build-c-server.sh => build-cpp-server.sh} | 2 +- 3 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 .github/actions/install-boost/action.yml rename scripts/{build-c-server.sh => build-cpp-server.sh} (90%) diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index 20e3b1e..e38a620 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -12,16 +12,20 @@ runs: with: luaVersion: ${{ inputs.lua-version }} - uses: leafo/gh-actions-luarocks@e65774a6386cb4f24e293dca7fc4ff89165b64c5 - - name: Install C Server-side SDK Dependencies + - uses: ./.github/actions/install-boost + id: install-boost + - name: Install C++ Server-side SDK Dependencies shell: bash run: | sudo apt-get update -y sudo apt-get install -y libcurl4-openssl-dev sudo apt-get install -y libhiredis-dev - - name: Setup C Server-side SDK + - name: Setup C++ Server-side SDK shell: bash - run: ./scripts/build-c-server.sh launchdarkly-cpp-server-redis-source-v1.0.0 + run: ./scripts/build-cpp-server.sh launchdarkly-cpp-server-redis-source-v1.0.0 + env: + BOOST_ROOT: ${{ steps.install-boost.outputs.BOOST_ROOT }} - name: Build Lua Server-side SDK shell: bash diff --git a/.github/actions/install-boost/action.yml b/.github/actions/install-boost/action.yml new file mode 100644 index 0000000..8794b45 --- /dev/null +++ b/.github/actions/install-boost/action.yml @@ -0,0 +1,26 @@ +# This custom action is used because the MarkusJx/install-boost does +# not include all built libraries for mac or windows builds. +# Specifically, it was missing boost_json. +name: Install Boost +description: 'Install boost.' +inputs: + platform_version: + required: false + default: "22.04" + toolset: + required: false + +outputs: + BOOST_ROOT: + description: The location of the installed boost. + value: ${{ steps.boost-action.outputs.BOOST_ROOT }} +runs: + using: composite + steps: + - name: Install boost using action + uses: MarkusJx/install-boost@v2.4.4 + id: boost-action + with: + boost_version: 1.81.0 + toolset: ${{ inputs.toolset }} + platform_version: ${{ inputs.platform_version }} diff --git a/scripts/build-c-server.sh b/scripts/build-cpp-server.sh similarity index 90% rename from scripts/build-c-server.sh rename to scripts/build-cpp-server.sh index 4cda090..b3e09cf 100755 --- a/scripts/build-c-server.sh +++ b/scripts/build-cpp-server.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Usage: ./scripts/build-c-server.sh +# Usage: ./scripts/build-cpp-server.sh set -e From aa431367d2a2eb44600426eae4f5a4839a08c98e Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 19 Dec 2023 09:19:27 -0800 Subject: [PATCH 082/161] fix bindings --- launchdarkly-server-sdk-redis-1.0-0.rockspec | 2 +- launchdarkly-server-sdk-redis.c | 25 ++++++++------------ 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/launchdarkly-server-sdk-redis-1.0-0.rockspec b/launchdarkly-server-sdk-redis-1.0-0.rockspec index 5305893..3dd314b 100644 --- a/launchdarkly-server-sdk-redis-1.0-0.rockspec +++ b/launchdarkly-server-sdk-redis-1.0-0.rockspec @@ -12,7 +12,7 @@ dependencies = { external_dependencies = { LDREDIS = { - header = "launchdarkly/server_side/integrations/redis/redis_source.hpp" + header = "launchdarkly/server_side/bindings/c/integrations/redis/redis_source.h" } } diff --git a/launchdarkly-server-sdk-redis.c b/launchdarkly-server-sdk-redis.c index 33625de..58b1bc6 100644 --- a/launchdarkly-server-sdk-redis.c +++ b/launchdarkly-server-sdk-redis.c @@ -9,8 +9,7 @@ Server-side SDK for LaunchDarkly Redis store. #include #include -#include -#include +#include @@ -33,20 +32,16 @@ LuaLDRedisMakeSource(lua_State *const l) const char* uri = luaL_checkstring(l, 1); const char* prefix = luaL_checkstring(l, 2); - LDServerSDK_RedisSource out_source; - LDStatus status = LDServerSDK_RedisSource_Create(uri, prefix, &out_source); - if (!LDStatus_Ok(status)) { - lua_pushstring(l, LDStatus_Error(status)); - LDStatus_Free(status); - return luaL_error(l, "failed to create Redis source: " .. lua_tostring(l, -1)); + struct LDServerLazyLoadRedisResult out_result; + bool success = LDServerLazyLoadRedisSource_New(uri, prefix, &out_result); + if (!success) { + return luaL_error(l, "failed to create Redis source: %s", out_result.error_message); } - storeInterface = LDStoreInterfaceRedisNew(config); + LDServerLazyLoadRedisSource *i = + (LDServerLazyLoadRedisSource *) lua_newuserdata(l, sizeof(out_result.source)); - struct LDStoreInterface **i = - (struct LDStoreInterface **)lua_newuserdata(l, sizeof(storeInterface)); - - *i = storeInterface; + *i = out_result.source; luaL_getmetatable(l, "LaunchDarklyStoreInterface"); lua_setmetatable(l, -2); @@ -55,7 +50,7 @@ LuaLDRedisMakeSource(lua_State *const l) } static const struct luaL_Reg launchdarkly_functions[] = { - { "makeStore", LuaLDRedisMakeStore }, + { "makeStore", LuaLDRedisMakeSource }, { NULL, NULL } }; @@ -80,7 +75,7 @@ static void luaL_setfuncs (lua_State *L, const luaL_Reg *l, int nup) { int luaopen_launchdarkly_server_sdk_redis(lua_State *const l) { - #if LUA_VERSION_NUM == 503 || LUA_VERSION_NUM == 502 + #if LUA_VERSION_NUM >= 502 luaL_newlib(l, launchdarkly_functions); #else luaL_register(l, "launchdarkly-server-sdk-redis", From 39d16ef3bc867a2bd59054b15eefd531ef3f848f Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 18 Dec 2023 15:40:17 -0800 Subject: [PATCH 083/161] try using prebuilt artifacts --- .github/actions/ci/action.yml | 12 +++------ .github/actions/install-boost/action.yml | 26 ------------------ .../actions/install-cpp-sdk-redis/action.yml | 27 +++++++++++++++++++ 3 files changed, 31 insertions(+), 34 deletions(-) delete mode 100644 .github/actions/install-boost/action.yml create mode 100644 .github/actions/install-cpp-sdk-redis/action.yml diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index e38a620..1d7a0d9 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -12,8 +12,10 @@ runs: with: luaVersion: ${{ inputs.lua-version }} - uses: leafo/gh-actions-luarocks@e65774a6386cb4f24e293dca7fc4ff89165b64c5 - - uses: ./.github/actions/install-boost - id: install-boost + - uses: ./.github/actions/install-cpp-sdk-redis + with: + version: v1.0.1 + path: cpp-sdk - name: Install C++ Server-side SDK Dependencies shell: bash run: | @@ -21,12 +23,6 @@ runs: sudo apt-get install -y libcurl4-openssl-dev sudo apt-get install -y libhiredis-dev - - name: Setup C++ Server-side SDK - shell: bash - run: ./scripts/build-cpp-server.sh launchdarkly-cpp-server-redis-source-v1.0.0 - env: - BOOST_ROOT: ${{ steps.install-boost.outputs.BOOST_ROOT }} - - name: Build Lua Server-side SDK shell: bash run: luarocks make launchdarkly-server-sdk-1.0-0.rockspec diff --git a/.github/actions/install-boost/action.yml b/.github/actions/install-boost/action.yml deleted file mode 100644 index 8794b45..0000000 --- a/.github/actions/install-boost/action.yml +++ /dev/null @@ -1,26 +0,0 @@ -# This custom action is used because the MarkusJx/install-boost does -# not include all built libraries for mac or windows builds. -# Specifically, it was missing boost_json. -name: Install Boost -description: 'Install boost.' -inputs: - platform_version: - required: false - default: "22.04" - toolset: - required: false - -outputs: - BOOST_ROOT: - description: The location of the installed boost. - value: ${{ steps.boost-action.outputs.BOOST_ROOT }} -runs: - using: composite - steps: - - name: Install boost using action - uses: MarkusJx/install-boost@v2.4.4 - id: boost-action - with: - boost_version: 1.81.0 - toolset: ${{ inputs.toolset }} - platform_version: ${{ inputs.platform_version }} diff --git a/.github/actions/install-cpp-sdk-redis/action.yml b/.github/actions/install-cpp-sdk-redis/action.yml new file mode 100644 index 0000000..e353b2d --- /dev/null +++ b/.github/actions/install-cpp-sdk-redis/action.yml @@ -0,0 +1,27 @@ +# This custom action is used because the MarkusJx/install-boost does +# not include all built libraries for mac or windows builds. +# Specifically, it was missing boost_json. +name: Install C++ Server-side SDK with Redis Source +description: 'Obtain prebuilt libraries for the Launchdarkly C++ server-side SDK with Redis Source.' +inputs: + version: + required: true + description: "Version of the C++ SDK with Redis Source." + default: "v1.0.1" + path: + description: "Where to download the SDK." + default: "cpp-sdk" + +runs: + using: composite + steps: + - name: Download C++ Server-side SDK with Redis Source + uses: robinraju/release-downloader@efa4cd07bd0195e6cc65e9e30c251b49ce4d3e51 + id: download-cpp-sdk + with: + repository: "launchdarkly/cpp-sdks" + latest: false + tag: launchdarkly-cpp-server-redis-source-${{ inputs.version }} + fileName: linux-gcc-x64-dynamic.zip + out-file-path: ${{ inputs.path }} + extract: true From a7ed3b66ac0d132ed8edfd03fb88d0f4cbbc8b28 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 18 Dec 2023 15:43:31 -0800 Subject: [PATCH 084/161] add install dir --- .github/actions/ci/action.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index 1d7a0d9..e6e98c6 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -25,11 +25,11 @@ runs: - name: Build Lua Server-side SDK shell: bash - run: luarocks make launchdarkly-server-sdk-1.0-0.rockspec + run: luarocks make launchdarkly-server-sdk-1.0-0.rockspec LD_DIR=./cpp-sdk - name: Build Lua Server-side SDK with Redis shell: bash - run: luarocks make launchdarkly-server-sdk-redis-1.0-0.rockspec + run: luarocks make launchdarkly-server-sdk-redis-1.0-0.rockspec LD_DIR=./cpp-sdk - name: Run tests shell: bash From 9aadad454b8e11e0e1365465710143e9caa88b6c Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 18 Dec 2023 15:47:10 -0800 Subject: [PATCH 085/161] add release subdir --- .github/actions/ci/action.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index e6e98c6..c6181a5 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -25,11 +25,11 @@ runs: - name: Build Lua Server-side SDK shell: bash - run: luarocks make launchdarkly-server-sdk-1.0-0.rockspec LD_DIR=./cpp-sdk + run: luarocks make launchdarkly-server-sdk-1.0-0.rockspec LD_DIR=./cpp-sdk/release - name: Build Lua Server-side SDK with Redis shell: bash - run: luarocks make launchdarkly-server-sdk-redis-1.0-0.rockspec LD_DIR=./cpp-sdk + run: luarocks make launchdarkly-server-sdk-redis-1.0-0.rockspec LD_DIR=./cpp-sdk/release - name: Run tests shell: bash From 0f5b40829e0795da75c5b8e6b17d24041bf2fdf6 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 18 Dec 2023 16:16:16 -0800 Subject: [PATCH 086/161] debug downloaded files --- .github/actions/ci/action.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index c6181a5..48f0c7c 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -16,6 +16,10 @@ runs: with: version: v1.0.1 path: cpp-sdk + - name: Inspect downloaded files + shell: bash + run: | + ls -R ./cpp-sdk - name: Install C++ Server-side SDK Dependencies shell: bash run: | @@ -25,11 +29,11 @@ runs: - name: Build Lua Server-side SDK shell: bash - run: luarocks make launchdarkly-server-sdk-1.0-0.rockspec LD_DIR=./cpp-sdk/release + run: luarocks make launchdarkly-server-sdk-1.0-0.rockspec LD_DIR=./cpp-sdk/build-dynamic/release - name: Build Lua Server-side SDK with Redis shell: bash - run: luarocks make launchdarkly-server-sdk-redis-1.0-0.rockspec LD_DIR=./cpp-sdk/release + run: luarocks make launchdarkly-server-sdk-redis-1.0-0.rockspec LD_DIR=./cpp-sdk/build-dynamic/release - name: Run tests shell: bash From d59524900f6e394ddaaac3418a88f94382359f64 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 18 Dec 2023 16:19:40 -0800 Subject: [PATCH 087/161] update redis dir --- .github/actions/ci/action.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index 48f0c7c..ea168b0 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -16,10 +16,6 @@ runs: with: version: v1.0.1 path: cpp-sdk - - name: Inspect downloaded files - shell: bash - run: | - ls -R ./cpp-sdk - name: Install C++ Server-side SDK Dependencies shell: bash run: | @@ -33,7 +29,7 @@ runs: - name: Build Lua Server-side SDK with Redis shell: bash - run: luarocks make launchdarkly-server-sdk-redis-1.0-0.rockspec LD_DIR=./cpp-sdk/build-dynamic/release + run: luarocks make launchdarkly-server-sdk-redis-1.0-0.rockspec LDREDIS_DIR=./cpp-sdk/build-dynamic/release - name: Run tests shell: bash From 638c31492f5440d39c1303427db268121ff006b0 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 18 Dec 2023 16:42:35 -0800 Subject: [PATCH 088/161] use prerelease tags --- .github/actions/ci/action.yml | 2 +- .github/actions/install-cpp-sdk-redis/action.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index ea168b0..28c2132 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -14,7 +14,7 @@ runs: - uses: leafo/gh-actions-luarocks@e65774a6386cb4f24e293dca7fc4ff89165b64c5 - uses: ./.github/actions/install-cpp-sdk-redis with: - version: v1.0.1 + version: redis-c-binding-prerelease-v1.1.0 path: cpp-sdk - name: Install C++ Server-side SDK Dependencies shell: bash diff --git a/.github/actions/install-cpp-sdk-redis/action.yml b/.github/actions/install-cpp-sdk-redis/action.yml index e353b2d..014d9e6 100644 --- a/.github/actions/install-cpp-sdk-redis/action.yml +++ b/.github/actions/install-cpp-sdk-redis/action.yml @@ -21,7 +21,7 @@ runs: with: repository: "launchdarkly/cpp-sdks" latest: false - tag: launchdarkly-cpp-server-redis-source-${{ inputs.version }} + tag: ${{ inputs.version }} fileName: linux-gcc-x64-dynamic.zip out-file-path: ${{ inputs.path }} extract: true From 35f05055a2d81ff80e6aeab49f4579cd5f2433b2 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 19 Dec 2023 09:25:47 -0800 Subject: [PATCH 089/161] update prebuilt artifact tag --- .github/actions/ci/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index 28c2132..1752356 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -14,7 +14,7 @@ runs: - uses: leafo/gh-actions-luarocks@e65774a6386cb4f24e293dca7fc4ff89165b64c5 - uses: ./.github/actions/install-cpp-sdk-redis with: - version: redis-c-binding-prerelease-v1.1.0 + version: redis-source-bindings-v1.1.0 path: cpp-sdk - name: Install C++ Server-side SDK Dependencies shell: bash From f7c94138e9cd5c3e3b91a65af206e5c5880cb235 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 19 Dec 2023 09:28:05 -0800 Subject: [PATCH 090/161] run CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c272cb..0e54306 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,7 +5,7 @@ on: paths-ignore: - '**.md' # Do not need to run CI for markdown changes. pull_request: - branches: [ main ] + branches: [ 'main', 'cw/sc-227439/updated-c-bindings' ] paths-ignore: - '**.md' From 2de15df98cde9b2e1c092f858ad62ed82ecf0ee4 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 19 Dec 2023 09:57:10 -0800 Subject: [PATCH 091/161] fix compilation --- launchdarkly-server-sdk.c | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 63a95b6..6d0a34b 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -484,27 +484,25 @@ lazyLoad = { lua_getfield(l, i, "lazyLoad"); if (lua_isnil(l, -1)) { - return luaL_error(l, "dataSystem.lazyLoad specified, must specify a source"); + luaL_error(l, "dataSystem.lazyLoad specified, must specify a source"); } else if (lua_istable(l, -1)) { lua_getfield(l, i, "source"); if (lua_isnil(l, -1)) { - return luaL_error(l, "dataSystem.lazyLoad.source must be provided"); + luaL_error(l, "dataSystem.lazyLoad.source must be provided"); } - LDServerLazyLoadSource *source = luaL_checkudata(l, 1, "LaunchDarklyStoreInterface"); + LDServerLazyLoadSourcePtr *source = luaL_checkudata(l, 1, "LaunchDarklyStoreInterface"); LDServerLazyLoadBuilder lazy_load_builder = LDServerLazyLoadBuilder_New(); - LDServerLazyLoadBuilder_Source(lazy_load_builder, source); - LDServerLazyLoadBuilder_CacheRefreshMs(lazy_load_builder, 30000); - LDServerLazyLoadBuilder_CacheEviction(lazy_load_builder, LDServerLazyLoadCacheEviction_Disabled); + LDServerLazyLoadBuilder_SourcePtr(lazy_load_builder, *source); /* This should be a metatable containing the redis source */ } else { - return luaL_error(l, "dataSystem.lazyLoad must be a table"); + luaL_error(l, "dataSystem.lazyLoad must be a table"); } } else if (lua_istable(l, -1)) { lua_getfield(l, i, "source"); @@ -530,13 +528,13 @@ lazyLoad = { LDServerConfigBuilder_DataSystem_BackgroundSync_Polling(builder, poll_builder); } else { - return luaL_error(l, "dataSystem.method must be 'streaming' or 'polling'"); + luaL_error(l, "dataSystem.method must be 'streaming' or 'polling'"); } } else { - return luaL_error(l, "dataSystem.backgroundSync.source must be a string"); + luaL_error(l, "dataSystem.backgroundSync.source must be a string"); } } else { - return luaL_error(l, "dataSystem.backgroundSync must be a table"); + luaL_error(l, "dataSystem.backgroundSync must be a table"); } } From 22a4209666222f7d66f549ed925c9c9095a6412f Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 19 Dec 2023 10:10:33 -0800 Subject: [PATCH 092/161] add boost to dependencies --- .github/actions/ci/action.yml | 33 ++++++++++++++++++++------ launchdarkly-server-sdk-1.0-0.rockspec | 16 ++++++++++++- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index 1752356..c0e7481 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -8,11 +8,20 @@ inputs: runs: using: composite steps: - - uses: leafo/gh-actions-lua@35bcb06abec04ec87df82e08caa84d545348536e + - name: Install Lua + uses: leafo/gh-actions-lua@35bcb06abec04ec87df82e08caa84d545348536e with: luaVersion: ${{ inputs.lua-version }} - - uses: leafo/gh-actions-luarocks@e65774a6386cb4f24e293dca7fc4ff89165b64c5 - - uses: ./.github/actions/install-cpp-sdk-redis + - name: Install LuaRocks + uses: leafo/gh-actions-luarocks@e65774a6386cb4f24e293dca7fc4ff89165b64c5 + - name: Install Boost + id: install-boost + uses: MarkusJx/install-boost@v2.4.4 + with: + boost_version: 1.81.0 + platform_version: "22.04" + - name: Install CPP SDK + uses: ./.github/actions/install-cpp-sdk-redis with: version: redis-source-bindings-v1.1.0 path: cpp-sdk @@ -20,23 +29,33 @@ runs: shell: bash run: | sudo apt-get update -y - sudo apt-get install -y libcurl4-openssl-dev sudo apt-get install -y libhiredis-dev - name: Build Lua Server-side SDK shell: bash - run: luarocks make launchdarkly-server-sdk-1.0-0.rockspec LD_DIR=./cpp-sdk/build-dynamic/release + run: | + luarocks make launchdarkly-server-sdk-1.0-0.rockspec \ + LD_DIR=./cpp-sdk/build-dynamic/release \ + BOOST_DIR=${{ steps.install-boost.outputs.BOOST_ROOT }} - name: Build Lua Server-side SDK with Redis shell: bash - run: luarocks make launchdarkly-server-sdk-redis-1.0-0.rockspec LDREDIS_DIR=./cpp-sdk/build-dynamic/release + run: | + luarocks make launchdarkly-server-sdk-redis-1.0-0.rockspec \ + LDREDIS_DIR=./cpp-sdk/build-dynamic/release \ + LD_DIR=./cpp-sdk/build-dynamic/release \ + BOOST_DIR=${{ steps.install-boost.outputs.BOOST_ROOT }} - name: Run tests shell: bash if: ${{ ! contains(inputs.lua-version, 'jit') }} - run: lua test.lua + run: luarocks test launchdarkly-server-sdk-1.0-0.rockspec + env: + LD_LIBRARY_PATH: ${{ steps.install-boost.outputs.BOOST_ROOT }}/lib - name: Run tests (JIT) shell: bash if: ${{ contains(inputs.lua-version, 'jit') }} run: luajit test.lua + env: + LD_LIBRARY_PATH: ${{ steps.install-boost.outputs.BOOST_ROOT }}/lib diff --git a/launchdarkly-server-sdk-1.0-0.rockspec b/launchdarkly-server-sdk-1.0-0.rockspec index 24325e1..c89788d 100644 --- a/launchdarkly-server-sdk-1.0-0.rockspec +++ b/launchdarkly-server-sdk-1.0-0.rockspec @@ -1,5 +1,7 @@ package = "launchdarkly-server-sdk" +rockspec_format = "3.0" + version = "1.0-0" source = { @@ -14,16 +16,28 @@ dependencies = { external_dependencies = { LD = { header = "launchdarkly/server_side/bindings/c/sdk.h" + }, + platforms = { + linux = { + BOOST= { + library = "libboost_json-mt-x64.so.1.81.0" + } + } } } +test = { + type = "command", + script = "test.lua" +} + build = { type = "builtin", modules = { ["launchdarkly_server_sdk"] = { sources = { "launchdarkly-server-sdk.c" }, incdirs = {"$(LD_INCDIR)"}, - libdirs = {"$(LD_LIBDIR)"}, + libdirs = {"$(LD_LIBDIR)", "$(BOOST_LIBDIR)"}, libraries = {"launchdarkly-cpp-server"} } } From 6e92dfcc909d06016e0bd07db0adcc8240f980dc Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 19 Dec 2023 15:14:45 -0800 Subject: [PATCH 093/161] try removing BOOST_LIBDIR frm libdirs --- .github/actions/ci/action.yml | 4 ++++ launchdarkly-server-sdk-1.0-0.rockspec | 6 +++++- launchdarkly-server-sdk-redis-1.0-0.rockspec | 5 +++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index c0e7481..39ae164 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -51,6 +51,8 @@ runs: if: ${{ ! contains(inputs.lua-version, 'jit') }} run: luarocks test launchdarkly-server-sdk-1.0-0.rockspec env: + # Needed because boost isn't installed in default system paths, which is + # what the C++ Server-side SDK shared objec expects. LD_LIBRARY_PATH: ${{ steps.install-boost.outputs.BOOST_ROOT }}/lib - name: Run tests (JIT) @@ -58,4 +60,6 @@ runs: if: ${{ contains(inputs.lua-version, 'jit') }} run: luajit test.lua env: + # Needed because boost isn't installed in default system paths, which is + # what the C++ Server-side SDK shared object expects. LD_LIBRARY_PATH: ${{ steps.install-boost.outputs.BOOST_ROOT }}/lib diff --git a/launchdarkly-server-sdk-1.0-0.rockspec b/launchdarkly-server-sdk-1.0-0.rockspec index c89788d..8d4d13a 100644 --- a/launchdarkly-server-sdk-1.0-0.rockspec +++ b/launchdarkly-server-sdk-1.0-0.rockspec @@ -2,6 +2,8 @@ package = "launchdarkly-server-sdk" rockspec_format = "3.0" +supported_platforms = {"linux"} + version = "1.0-0" source = { @@ -20,6 +22,8 @@ external_dependencies = { platforms = { linux = { BOOST= { + -- The library depends on boost_json and boost_url, but we only need to test for the existence + -- of one of them since they are both part of the boost distribution. library = "libboost_json-mt-x64.so.1.81.0" } } @@ -37,7 +41,7 @@ build = { ["launchdarkly_server_sdk"] = { sources = { "launchdarkly-server-sdk.c" }, incdirs = {"$(LD_INCDIR)"}, - libdirs = {"$(LD_LIBDIR)", "$(BOOST_LIBDIR)"}, + libdirs = {"$(LD_LIBDIR)"}, libraries = {"launchdarkly-cpp-server"} } } diff --git a/launchdarkly-server-sdk-redis-1.0-0.rockspec b/launchdarkly-server-sdk-redis-1.0-0.rockspec index 3dd314b..60530a7 100644 --- a/launchdarkly-server-sdk-redis-1.0-0.rockspec +++ b/launchdarkly-server-sdk-redis-1.0-0.rockspec @@ -1,7 +1,12 @@ package = "launchdarkly-server-sdk-redis" +rockspec_format = "3.0" + +supported_platforms = {"linux"} + version = "1.0-0" + source = { url = "git+https://github.com/launchdarkly/lua-server-sdk.git" } From 9005ed855f3ff51b1dd06d0a72582c47c08c4561 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 19 Dec 2023 15:21:13 -0800 Subject: [PATCH 094/161] Try adding ld-cpp-server to external dependencies --- launchdarkly-server-sdk-1.0-0.rockspec | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/launchdarkly-server-sdk-1.0-0.rockspec b/launchdarkly-server-sdk-1.0-0.rockspec index 8d4d13a..228c8fa 100644 --- a/launchdarkly-server-sdk-1.0-0.rockspec +++ b/launchdarkly-server-sdk-1.0-0.rockspec @@ -17,11 +17,12 @@ dependencies = { external_dependencies = { LD = { - header = "launchdarkly/server_side/bindings/c/sdk.h" + header = "launchdarkly/server_side/bindings/c/sdk.h", + library = "launchdarkly-cpp-server" }, platforms = { linux = { - BOOST= { + BOOST = { -- The library depends on boost_json and boost_url, but we only need to test for the existence -- of one of them since they are both part of the boost distribution. library = "libboost_json-mt-x64.so.1.81.0" From 6a107e3ca14908e265a278d73257e9c100120e56 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 19 Dec 2023 15:30:20 -0800 Subject: [PATCH 095/161] add redis test --- .github/actions/ci/action.yml | 22 ++++++++++++++++++-- launchdarkly-server-sdk-redis-1.0-0.rockspec | 5 +++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index 39ae164..8fde45d 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -46,7 +46,7 @@ runs: LD_DIR=./cpp-sdk/build-dynamic/release \ BOOST_DIR=${{ steps.install-boost.outputs.BOOST_ROOT }} - - name: Run tests + - name: Run Lua Server-side SDK Tests shell: bash if: ${{ ! contains(inputs.lua-version, 'jit') }} run: luarocks test launchdarkly-server-sdk-1.0-0.rockspec @@ -55,7 +55,7 @@ runs: # what the C++ Server-side SDK shared objec expects. LD_LIBRARY_PATH: ${{ steps.install-boost.outputs.BOOST_ROOT }}/lib - - name: Run tests (JIT) + - name: Run Lua Server-side SDK Tests (JIT) shell: bash if: ${{ contains(inputs.lua-version, 'jit') }} run: luajit test.lua @@ -63,3 +63,21 @@ runs: # Needed because boost isn't installed in default system paths, which is # what the C++ Server-side SDK shared object expects. LD_LIBRARY_PATH: ${{ steps.install-boost.outputs.BOOST_ROOT }}/lib + + - name: Run Lua Server-side SDK with Redis Tests + shell: bash + if: ${{ ! contains(inputs.lua-version, 'jit') }} + run: luarocks test launchdarkly-server-sdk-redis-1.0-0.rockspec + env: + # Needed because boost isn't installed in default system paths, which is + # what the C++ Server-side SDK shared objec expects. + LD_LIBRARY_PATH: ${{ steps.install-boost.outputs.BOOST_ROOT }}/lib + + - name: Run Lua Server-side SDK with Redis Tests (JIT) + shell: bash + if: ${{ contains(inputs.lua-version, 'jit') }} + run: luajit redis-test.lua + env: + # Needed because boost isn't installed in default system paths, which is + # what the C++ Server-side SDK shared object expects. + LD_LIBRARY_PATH: ${{ steps.install-boost.outputs.BOOST_ROOT }}/lib diff --git a/launchdarkly-server-sdk-redis-1.0-0.rockspec b/launchdarkly-server-sdk-redis-1.0-0.rockspec index 60530a7..3767ba9 100644 --- a/launchdarkly-server-sdk-redis-1.0-0.rockspec +++ b/launchdarkly-server-sdk-redis-1.0-0.rockspec @@ -21,6 +21,11 @@ external_dependencies = { } } +test = { + type = "command", + script = "redis-test.lua" +} + build = { type = "builtin", modules = { From 9e5656156b73e714ff16e0f1bcdf7bddd4f69686 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 19 Dec 2023 15:35:49 -0800 Subject: [PATCH 096/161] update redis test --- launchdarkly-server-sdk-redis.c | 2 +- redis-test.lua | 58 +++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 redis-test.lua diff --git a/launchdarkly-server-sdk-redis.c b/launchdarkly-server-sdk-redis.c index 58b1bc6..a90f38b 100644 --- a/launchdarkly-server-sdk-redis.c +++ b/launchdarkly-server-sdk-redis.c @@ -50,7 +50,7 @@ LuaLDRedisMakeSource(lua_State *const l) } static const struct luaL_Reg launchdarkly_functions[] = { - { "makeStore", LuaLDRedisMakeSource }, + { "makeRedisSource", LuaLDRedisMakeSource }, { NULL, NULL } }; diff --git a/redis-test.lua b/redis-test.lua new file mode 100644 index 0000000..623ca3e --- /dev/null +++ b/redis-test.lua @@ -0,0 +1,58 @@ +local u = require('luaunit') +local r = require("launchdarkly_server_sdk_redis") + +function logWrite(level, line) + print("[" .. level .. "]" .. " " .. line) +end + +function logEnabled(level) + return level == "warn" or level == "error" +end + +l.registerLogger(logWrite, logEnabled) + +TestAll = {} + +function makeTestClient() + local c = l.clientInit({ + key = "sdk-test", + offline = True + }, 0) + + return c +end + +local user = l.makeUser({ + key = "alice", + dataSystem = { + enabled = true, + method = "streaming", + params = { + initialReconnectDelayMs = 1000 + } + } +}) + +function TestAll:tearDown() + collectgarbage("collect") +end + +function TestAll:testRedisBasic() + local c = l.clientInit({ + key = "sdk-test", + dataSystem = { + backgroundSync = { + lazyLoad = { + source = makeRedisSource("redis://localhost:1234", "test-prefix") + } + } + } + }, 0) + + local e = false + + u.assertEquals(e, c:boolVariation(user, "test", e)) +end + +local runner = u.LuaUnit.new() +os.exit(runner:runSuite()) From b75c2f7a8ada013e7f440e87e4d5360a0ee40726 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 19 Dec 2023 15:44:49 -0800 Subject: [PATCH 097/161] add HIREDIS path --- .github/actions/ci/action.yml | 3 ++- launchdarkly-server-sdk-redis-1.0-0.rockspec | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index 8fde45d..15abedf 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -44,7 +44,8 @@ runs: luarocks make launchdarkly-server-sdk-redis-1.0-0.rockspec \ LDREDIS_DIR=./cpp-sdk/build-dynamic/release \ LD_DIR=./cpp-sdk/build-dynamic/release \ - BOOST_DIR=${{ steps.install-boost.outputs.BOOST_ROOT }} + BOOST_DIR=${{ steps.install-boost.outputs.BOOST_ROOT }} \ + HIREDIS_DIR=./cpp-sdk/build-dynamic/release - name: Run Lua Server-side SDK Tests shell: bash diff --git a/launchdarkly-server-sdk-redis-1.0-0.rockspec b/launchdarkly-server-sdk-redis-1.0-0.rockspec index 3767ba9..cfcfc70 100644 --- a/launchdarkly-server-sdk-redis-1.0-0.rockspec +++ b/launchdarkly-server-sdk-redis-1.0-0.rockspec @@ -17,7 +17,15 @@ dependencies = { external_dependencies = { LDREDIS = { - header = "launchdarkly/server_side/bindings/c/integrations/redis/redis_source.h" + header = "launchdarkly/server_side/bindings/c/integrations/redis/redis_source.h", + library = "launchdarkly-cpp-server-redis-source" + }, + platforms = { + linux = { + HIREDIS = { + library = "libhiredis.so.1.1.0" + } + } } } From da4567a9c21372185a7ff8e472be41c391e64757 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 19 Dec 2023 15:53:36 -0800 Subject: [PATCH 098/161] try using modules.libraries for hiredis --- .github/actions/ci/action.yml | 3 +-- launchdarkly-server-sdk-redis-1.0-0.rockspec | 9 +-------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index 15abedf..8fde45d 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -44,8 +44,7 @@ runs: luarocks make launchdarkly-server-sdk-redis-1.0-0.rockspec \ LDREDIS_DIR=./cpp-sdk/build-dynamic/release \ LD_DIR=./cpp-sdk/build-dynamic/release \ - BOOST_DIR=${{ steps.install-boost.outputs.BOOST_ROOT }} \ - HIREDIS_DIR=./cpp-sdk/build-dynamic/release + BOOST_DIR=${{ steps.install-boost.outputs.BOOST_ROOT }} - name: Run Lua Server-side SDK Tests shell: bash diff --git a/launchdarkly-server-sdk-redis-1.0-0.rockspec b/launchdarkly-server-sdk-redis-1.0-0.rockspec index cfcfc70..d2e8f3e 100644 --- a/launchdarkly-server-sdk-redis-1.0-0.rockspec +++ b/launchdarkly-server-sdk-redis-1.0-0.rockspec @@ -19,13 +19,6 @@ external_dependencies = { LDREDIS = { header = "launchdarkly/server_side/bindings/c/integrations/redis/redis_source.h", library = "launchdarkly-cpp-server-redis-source" - }, - platforms = { - linux = { - HIREDIS = { - library = "libhiredis.so.1.1.0" - } - } } } @@ -41,7 +34,7 @@ build = { sources = { "launchdarkly-server-sdk-redis.c" }, incdirs = {"$(LDREDIS_INCDIR)"}, libdirs = {"$(LDREDIS_LIBDIR)"}, - libraries = {"launchdarkly-cpp-server-redis-source"} + libraries = {"launchdarkly-cpp-server-redis-source", "hiredis"} } } } From e58ef901b1b326130079b85e39137e9f52566941 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 19 Dec 2023 16:26:45 -0800 Subject: [PATCH 099/161] attempt to fix build --- .github/actions/ci/action.yml | 16 ++++++---------- launchdarkly-server-sdk-redis-1.0-0.rockspec | 2 +- redis-test.lua | 3 ++- test.lua | 1 - 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index 8fde45d..e34180b 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -25,11 +25,6 @@ runs: with: version: redis-source-bindings-v1.1.0 path: cpp-sdk - - name: Install C++ Server-side SDK Dependencies - shell: bash - run: | - sudo apt-get update -y - sudo apt-get install -y libhiredis-dev - name: Build Lua Server-side SDK shell: bash @@ -43,7 +38,6 @@ runs: run: | luarocks make launchdarkly-server-sdk-redis-1.0-0.rockspec \ LDREDIS_DIR=./cpp-sdk/build-dynamic/release \ - LD_DIR=./cpp-sdk/build-dynamic/release \ BOOST_DIR=${{ steps.install-boost.outputs.BOOST_ROOT }} - name: Run Lua Server-side SDK Tests @@ -70,8 +64,9 @@ runs: run: luarocks test launchdarkly-server-sdk-redis-1.0-0.rockspec env: # Needed because boost isn't installed in default system paths, which is - # what the C++ Server-side SDK shared objec expects. - LD_LIBRARY_PATH: ${{ steps.install-boost.outputs.BOOST_ROOT }}/lib + # what the C++ Server-side SDK shared object expects. Same for hiredis which is bundled + # with the SDK release. + LD_LIBRARY_PATH: ${{ steps.install-boost.outputs.BOOST_ROOT }}/lib;./cpp-sdk/build-dynamic/release/lib - name: Run Lua Server-side SDK with Redis Tests (JIT) shell: bash @@ -79,5 +74,6 @@ runs: run: luajit redis-test.lua env: # Needed because boost isn't installed in default system paths, which is - # what the C++ Server-side SDK shared object expects. - LD_LIBRARY_PATH: ${{ steps.install-boost.outputs.BOOST_ROOT }}/lib + # what the C++ Server-side SDK shared object expects. Same for hiredis which is bundled + # with the SDK release. + LD_LIBRARY_PATH: ${{ steps.install-boost.outputs.BOOST_ROOT }}/lib;./cpp-sdk/build-dynamic/release/lib diff --git a/launchdarkly-server-sdk-redis-1.0-0.rockspec b/launchdarkly-server-sdk-redis-1.0-0.rockspec index d2e8f3e..77be312 100644 --- a/launchdarkly-server-sdk-redis-1.0-0.rockspec +++ b/launchdarkly-server-sdk-redis-1.0-0.rockspec @@ -34,7 +34,7 @@ build = { sources = { "launchdarkly-server-sdk-redis.c" }, incdirs = {"$(LDREDIS_INCDIR)"}, libdirs = {"$(LDREDIS_LIBDIR)"}, - libraries = {"launchdarkly-cpp-server-redis-source", "hiredis"} + libraries = {"launchdarkly-cpp-server-redis-source"} } } } diff --git a/redis-test.lua b/redis-test.lua index 623ca3e..1eb6f99 100644 --- a/redis-test.lua +++ b/redis-test.lua @@ -1,4 +1,5 @@ local u = require('luaunit') +local l = require("launchdarkly_server_sdk") local r = require("launchdarkly_server_sdk_redis") function logWrite(level, line) @@ -43,7 +44,7 @@ function TestAll:testRedisBasic() dataSystem = { backgroundSync = { lazyLoad = { - source = makeRedisSource("redis://localhost:1234", "test-prefix") + source = r.makeRedisSource("redis://localhost:1234", "test-prefix") } } } diff --git a/test.lua b/test.lua index 4fa0a57..22a7e5f 100644 --- a/test.lua +++ b/test.lua @@ -1,6 +1,5 @@ local u = require('luaunit') local l = require("launchdarkly_server_sdk") --- local r = require("launchdarkly_server_sdk_redis") function logWrite(level, line) print("[" .. level .. "]" .. " " .. line) From 99c1ea45f96ef4ca044224e764dbb33e89917245 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 19 Dec 2023 16:30:58 -0800 Subject: [PATCH 100/161] fix invocation of makeRedisSource --- launchdarkly-server-sdk-redis.c | 2 +- launchdarkly-server-sdk.c | 27 --------------------------- redis-test.lua | 31 ++++++++----------------------- test.lua | 21 +-------------------- 4 files changed, 10 insertions(+), 71 deletions(-) diff --git a/launchdarkly-server-sdk-redis.c b/launchdarkly-server-sdk-redis.c index a90f38b..17d859e 100644 --- a/launchdarkly-server-sdk-redis.c +++ b/launchdarkly-server-sdk-redis.c @@ -24,7 +24,7 @@ of a LaunchDarkly Streaming or Polling data source. static int LuaLDRedisMakeSource(lua_State *const l) { - if (lua_gettop(l) != 1) { + if (lua_gettop(l) != 2) { return luaL_error(l, "expecting exactly 2 arguments"); } diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 6d0a34b..a53dbd1 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -411,17 +411,6 @@ LuaLDUserFree(lua_State *const l) return 0; } -// makeConfig({ -// dataSystem: { -// enabled: bool -// method: streaming -// -// -// } -// }) - - - static LDServerConfig makeConfig(lua_State *const l, const int i) { @@ -453,22 +442,6 @@ makeConfig(lua_State *const l, const int i) LDServerConfigBuilder_ServiceEndpoints_EventsBaseURL(builder, luaL_checkstring(l, -1)); } -/* - local user = l.makeUser({ - key = "alice", - dataSystem = { - enabled = true, - backgroundSync = { - source = "launchdarkly_streaming", - initialReconnectDelayMs = 1000 - } -lazyLoad = { - source = makeRedisSource("redis://localhost:6379", "prefix") -} - } - }) -*/ - // TODO: stream, useLDD, and pollInterval, and featureStoreBackend all unified under new dataSystem key lua_getfield(l, i, "dataSystem"); diff --git a/redis-test.lua b/redis-test.lua index 1eb6f99..9d224f5 100644 --- a/redis-test.lua +++ b/redis-test.lua @@ -16,22 +16,19 @@ TestAll = {} function makeTestClient() local c = l.clientInit({ - key = "sdk-test", - offline = True + key = "sdk-test", + dataSystem = { + lazyLoad = { + source = r.makeRedisSource('redis://localhost:1234', 'test-prefix') + } + } }, 0) return c end local user = l.makeUser({ - key = "alice", - dataSystem = { - enabled = true, - method = "streaming", - params = { - initialReconnectDelayMs = 1000 - } - } + key = "alice" }) function TestAll:tearDown() @@ -39,20 +36,8 @@ function TestAll:tearDown() end function TestAll:testRedisBasic() - local c = l.clientInit({ - key = "sdk-test", - dataSystem = { - backgroundSync = { - lazyLoad = { - source = r.makeRedisSource("redis://localhost:1234", "test-prefix") - } - } - } - }, 0) - local e = false - - u.assertEquals(e, c:boolVariation(user, "test", e)) + u.assertEquals(e, makeTestClient():boolVariation(user, "test", e)) end local runner = u.LuaUnit.new() diff --git a/test.lua b/test.lua index 22a7e5f..3b0255d 100644 --- a/test.lua +++ b/test.lua @@ -23,14 +23,7 @@ function makeTestClient() end local user = l.makeUser({ - key = "alice", - dataSystem = { - enabled = true, - method = "streaming", - params = { - initialReconnectDelayMs = 1000 - } - } + key = "alice" }) function TestAll:tearDown() @@ -126,18 +119,6 @@ function TestAll:testIdentify() makeTestClient():identify(user) end ---function TestAll:testRedisBasic() --- local c = l.clientInit({ --- key = "sdk-test", --- featureStoreBackend = r.makeStore({}), --- offline = true --- }, 0) --- --- local e = false --- --- u.assertEquals(e, c:boolVariation(user, "test", e)) ---end - function TestAll:testVersion() local version = l.version() u.assertNotIsNil(version) From b873c38abc41389977381f78eca0fae99a2a9902 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Dec 2023 12:23:32 -0800 Subject: [PATCH 101/161] using a table driven approach for arg parsing --- launchdarkly-server-sdk-1.0-0.rockspec | 2 +- launchdarkly-server-sdk-redis-1.0-0.rockspec | 2 +- launchdarkly-server-sdk.c | 398 +++++++++++------- redis-test.lua | 9 +- scripts/compile.sh | 5 + ...d-cpp-server.sh => get-cpp-sdk-locally.sh} | 8 +- test.lua | 17 +- 7 files changed, 269 insertions(+), 172 deletions(-) create mode 100755 scripts/compile.sh rename scripts/{build-cpp-server.sh => get-cpp-sdk-locally.sh} (59%) diff --git a/launchdarkly-server-sdk-1.0-0.rockspec b/launchdarkly-server-sdk-1.0-0.rockspec index 228c8fa..ffaa575 100644 --- a/launchdarkly-server-sdk-1.0-0.rockspec +++ b/launchdarkly-server-sdk-1.0-0.rockspec @@ -2,7 +2,7 @@ package = "launchdarkly-server-sdk" rockspec_format = "3.0" -supported_platforms = {"linux"} +-- supported_platforms = {"linux"} version = "1.0-0" diff --git a/launchdarkly-server-sdk-redis-1.0-0.rockspec b/launchdarkly-server-sdk-redis-1.0-0.rockspec index 77be312..8da7b01 100644 --- a/launchdarkly-server-sdk-redis-1.0-0.rockspec +++ b/launchdarkly-server-sdk-redis-1.0-0.rockspec @@ -2,7 +2,7 @@ package = "launchdarkly-server-sdk-redis" rockspec_format = "3.0" -supported_platforms = {"linux"} +-- supported_platforms = {"linux"} version = "1.0-0" diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index a53dbd1..378e1c4 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -8,6 +8,8 @@ Server-side SDK for LaunchDarkly. #include #include #include +#include +#include #include #include @@ -411,196 +413,280 @@ LuaLDUserFree(lua_State *const l) return 0; } -static LDServerConfig -makeConfig(lua_State *const l, const int i) -{ - LDServerConfigBuilder builder; - - luaL_checktype(l, i, LUA_TTABLE); - - lua_getfield(l, i, "key"); - - const char *const key = luaL_checkstring(l, -1); - - builder = LDServerConfigBuilder_New(key); - - lua_getfield(l, i, "baseURI"); +static void debug(lua_State *const l, const int i){ + int t = lua_type(l, -1); + switch (t) { - if (lua_isstring(l, -1)) { - LDServerConfigBuilder_ServiceEndpoints_PollingBaseURL(builder, luaL_checkstring(l, -1)); - } + case LUA_TSTRING: /* strings */ + printf("`%s'", lua_tostring(l, i)); + break; - lua_getfield(l, i, "streamURI"); + case LUA_TBOOLEAN: /* booleans */ + printf(lua_toboolean(l, i) ? "true" : "false"); + break; - if (lua_isstring(l, -1)) { - LDServerConfigBuilder_ServiceEndpoints_StreamingBaseURL(builder, luaL_checkstring(l, -1)); - } + case LUA_TNUMBER: /* numbers */ + printf("%g", lua_tonumber(l, i)); + break; - lua_getfield(l, i, "eventsURI"); + default: /* other values */ + printf("%s", lua_typename(l, t)); + break; - if (lua_isstring(l, -1)) { - LDServerConfigBuilder_ServiceEndpoints_EventsBaseURL(builder, luaL_checkstring(l, -1)); } - -// TODO: stream, useLDD, and pollInterval, and featureStoreBackend all unified under new dataSystem key - lua_getfield(l, i, "dataSystem"); - + printf("\n"); +} +static bool has_table(lua_State *const l, const int i, const char* name) { + lua_getfield(l, i, name); + debug(l, i); if (lua_istable(l, -1)) { - lua_getfield(l, i, "enabled"); - if (lua_isboolean(l, -1)) { - LDServerConfigBuilder_DataSystem_Enabled(builder, lua_toboolean(l, -1)); - } - - lua_getfield(l, i, "backgroundSync"); - if (lua_isnil(l, -1)) { - lua_pop(l, 1); /* not an error, perhaps lazyLoad was configured instead */ - - lua_getfield(l, i, "lazyLoad"); - if (lua_isnil(l, -1)) { - luaL_error(l, "dataSystem.lazyLoad specified, must specify a source"); - } else if (lua_istable(l, -1)) { - lua_getfield(l, i, "source"); - - if (lua_isnil(l, -1)) { - luaL_error(l, "dataSystem.lazyLoad.source must be provided"); - } - - LDServerLazyLoadSourcePtr *source = luaL_checkudata(l, 1, "LaunchDarklyStoreInterface"); - - LDServerLazyLoadBuilder lazy_load_builder = LDServerLazyLoadBuilder_New(); - - LDServerLazyLoadBuilder_SourcePtr(lazy_load_builder, *source); - - - /* This should be a metatable containing the redis source */ - - } else { - luaL_error(l, "dataSystem.lazyLoad must be a table"); - } - } else if (lua_istable(l, -1)) { - lua_getfield(l, i, "source"); - - if (lua_isstring(l, -1)) { - const char *const source = luaL_checkstring(l, -1); - if (strcmp(source, "launchdarkly_streaming") == 0) { - LDServerDataSourceStreamBuilder stream_builder = LDServerDataSourceStreamBuilder_New(); - - lua_getfield(l, i, "initialReconnectDelayMs"); - if (lua_isnumber(l, -1)) { - LDServerDataSourceStreamBuilder_InitialReconnectDelayMs(stream_builder, luaL_checkinteger(l, -1)); - } - - LDServerConfigBuilder_DataSystem_BackgroundSync_Streaming(builder, stream_builder); - } else if (strcmp(source, "launchdarkly_polling") == 0) { - LDServerDataSourcePollBuilder poll_builder = LDServerDataSourcePollBuilder_New(); - - lua_getfield(l, i, "intervalSeconds"); - if (lua_isnumber(l, -1)) { - LDServerDataSourcePollBuilder_IntervalS(poll_builder, luaL_checkinteger(l, -1)); - } - - LDServerConfigBuilder_DataSystem_BackgroundSync_Polling(builder, poll_builder); - } else { - luaL_error(l, "dataSystem.method must be 'streaming' or 'polling'"); - } - } else { - luaL_error(l, "dataSystem.backgroundSync.source must be a string"); - } - } else { - luaL_error(l, "dataSystem.backgroundSync must be a table"); - } + return true; } + lua_pop(l, 1); + return false; +} - lua_getfield(l, i, "sendEvents"); - - if (lua_isboolean(l, -1)) { - LDServerConfigBuilder_Events_Enabled(builder, lua_toboolean(l, -1)); +static bool has_string(lua_State *const l, const int i, const char* name) { + lua_getfield(l, i, name); + if (lua_isstring(l, -1)) { + return true; } + lua_pop(l, 1); + return false; +} - lua_getfield(l, i, "eventsCapacity"); - - if (lua_isnumber(l, -1)) { - LDServerConfigBuilder_Events_Capacity(builder, luaL_checkinteger(l, -1)); +static void require_string(lua_State *const l, const int i, const char* name, const char* full_path) { + lua_getfield(l, i, name); + if (!lua_isstring(l, -1)) { + luaL_error(l, "%s must be provided as a string", full_path ? full_path : name); } +} - lua_getfield(l, i, "timeout"); - - if (lua_isnumber(l, -1)) { - // TODO: Use timeout +static void require_field(lua_State *const l, const int i, const char* name, const char* full_path) { + lua_getfield(l, i, name); + if (lua_isnil(l, -1)) { + luaL_error(l, "%s must be provided", full_path ? full_path : name); } +} - lua_getfield(l, i, "flushInterval"); - - if (lua_isnumber(l, -1)) { - LDServerConfigBuilder_Events_FlushIntervalMs(builder, luaL_checkinteger(l, -1)); +static void dump_table(lua_State *const l, const int i) { + lua_pushnil(l); + while (lua_next(l, i) != 0) { + printf("%s: %s\n", + luaL_checkstring(l, -2), + lua_typename(l, lua_type(l, -1))); + lua_pop(l, 1); } +} - lua_getfield(l, i, "pollInterval"); - +static bool has_number(lua_State *const l, const int i, const char* name) { + lua_getfield(l, i, name); if (lua_isnumber(l, -1)) { - // TODO: Set poll data system + return true; } + lua_pop(l, 1); + return false; +} - lua_getfield(l, i, "offline"); - +static bool bool_or_default(lua_State *const l, const int i, const char* name, const bool default_value) { + bool result = default_value; + lua_getfield(l, i, name); if (lua_isboolean(l, -1)) { - // TODO: Document change in behavior (offline now disables events + data system) - LDServerConfigBuilder_Offline(builder, lua_toboolean(l, -1)); + result = lua_toboolean(l, -1); } + lua_pop(l, 1); + return result; +} - lua_getfield(l, i, "useLDD"); - if (lua_isboolean(l, -1)) { - // TODO: Warn that this needs to be setup - } +struct field_validator { + const char* key; + int type; + void (*parse) (lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data); + void *user_data; +}; - lua_getfield(l, i, "inlineUsersInEvents"); +static void parse_uri(lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data) { + const char *const uri = luaL_checkstring(l, -1); + void (*setter)(LDServerConfigBuilder, const char*) = user_data; + setter(builder, uri); +} - if (lua_isboolean(l, -1)) { - // TODO: warn this was removed - } +static void parse_bool(lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data) { + const bool value = lua_toboolean(l, -1); + void (*setter)(LDServerConfigBuilder, bool) = user_data; + setter(builder, value); +} - lua_getfield(l, i, "allAttributesPrivate"); +static void parse_number(lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data) { + const int value = lua_tointeger(l, -1); + void (*setter)(LDServerConfigBuilder, int) = user_data; + setter(builder, value); +} - if (lua_isboolean(l, -1)) { - LDServerConfigBuilder_Events_AllAttributesPrivate(builder, lua_toboolean(l, -1)); - } +struct field_validator top_level_fields[] = { + {"baseURI", LUA_TSTRING, parse_uri, LDServerConfigBuilder_ServiceEndpoints_PollingBaseURL}, + {"streamURI", LUA_TSTRING, parse_uri, LDServerConfigBuilder_ServiceEndpoints_StreamingBaseURL}, + {"eventsURI", LUA_TSTRING, parse_uri, LDServerConfigBuilder_ServiceEndpoints_EventsBaseURL}, + {"offline", LUA_TBOOLEAN, parse_bool, LDServerConfigBuilder_Offline}, + {"sendEvents", LUA_TBOOLEAN, parse_bool, LDServerConfigBuilder_Events_Enabled}, + {"contextKeysCapacity", LUA_TNUMBER, parse_number, NULL}, + {"allAttributesPrivate", LUA_TBOOLEAN, parse_bool, LDServerConfigBuilder_Events_AllAttributesPrivate}, + {"eventsCapacity", LUA_TNUMBER, parse_number, LDServerConfigBuilder_Events_Capacity}, + {"flushInterval", LUA_TNUMBER, parse_number, LDServerConfigBuilder_Events_FlushIntervalMs}, + {"privateAttributeNames", LUA_TTABLE}, + {"dataSystem", LUA_TTABLE} +}; - lua_getfield(l, i, "userKeysCapacity"); +// todo: appinfo - if (lua_isnumber(l, -1)) { - // TODO: We don't have a C binding for this yet - //LDConfigSetUserKeysCapacity(config, luaL_checkinteger(l, -1)); +#define ARR_SIZE(x) (sizeof(x) / sizeof(x[0])) + +struct field_validator * find_field(const char *key, struct field_validator *fields, int n) { + for (int i = 0; i < n; i++) { + if (strcmp(fields[i].key, key) == 0) { + return &fields[i]; + } } + return NULL; +} - lua_getfield(l, i, "featureStoreBackend"); +#define DEBUG 1 - if (lua_isuserdata(l, -1)) { - // TODO: Setup the store reader backend - // struct LDStoreInterface **storeInterface; - // - // storeInterface = (struct LDStoreInterface **) - // luaL_checkudata(l, -1, "LaunchDarklyStoreInterface"); - // - // LDConfigSetFeatureStoreBackend(config, *storeInterface); - } +#define debug_print(foo) do { if (DEBUG) printf(foo); } while (0) - lua_getfield(l, 1, "privateAttributeNames"); +void traverse_config(lua_State *const l, int i, LDServerConfigBuilder builder) { - if (lua_istable(l, -1)) { - int n = lua_tablelen(l, -1); + luaL_checktype(l, i, LUA_TTABLE); - for (int i = 1; i <= n; i++) { - lua_rawgeti(l, -1, i); + lua_pushnil(l); + while (lua_next(l, -2)) { + lua_pushvalue(l, -2); - if (lua_isstring(l, -1)) { - LDServerConfigBuilder_Events_PrivateAttribute(builder, luaL_checkstring(l, -1)); - } + const char* key = lua_tostring(l, -1); + int type = lua_type(l, -2); - lua_pop(l, 1); + struct field_validator *field = find_field(key, top_level_fields, ARR_SIZE(top_level_fields)); + if (field == NULL) { + luaL_error(l, "unrecognized config field: %s", key); + } + if (field->type != type) { + luaL_error(l, "config field %s must be a %s", key, lua_typename(l, field->type)); } + if (field->parse) { + field->parse(l, -2, builder, field->user_data); + } else { + luaL_error(l, "missing field parser for %s", key); + } + lua_pop(l, 2); } + lua_pop(l, 1); +} +static LDServerConfig +makeConfig(lua_State *const l) +{ + // We are passed two arguments. One string (sdk key), followed by + // a table of config options. + + const char* sdk_key = luaL_checkstring(l, 1); + LDServerConfigBuilder builder = LDServerConfigBuilder_New(sdk_key); + traverse_config(l, 2, builder); + + + // luaL_error(l, "nope"); + // // Base, stream, and eventsURIs are optional - but if one is set, all + // // must be set (enforced by C++ SDK.) + // if (has_string(l, i, "baseURI")) { + // LDServerConfigBuilder_ServiceEndpoints_PollingBaseURL(builder, luaL_checkstring(l, -1)); + // } + // if (has_string(l, i, "streamURI")) { + // LDServerConfigBuilder_ServiceEndpoints_StreamingBaseURL(builder, luaL_checkstring(l, -1)); + // } + // if (has_string(l, i, "eventsURI")) { + // LDServerConfigBuilder_ServiceEndpoints_EventsBaseURL(builder, luaL_checkstring(l, -1)); + // } + // + // + // LDServerConfigBuilder_Events_Enabled(builder, bool_or_default(l, i, "sendEvents", true)); + // + // if (has_number(l, i, "eventsCapacity")) { + // LDServerConfigBuilder_Events_Capacity(builder, luaL_checkinteger(l, -1)); + // } + // if (has_number(l, i, "flushInterval")) { + // LDServerConfigBuilder_Events_FlushIntervalMs(builder, luaL_checkinteger(l, -1)); + // } + // + // + // + // if (has_table(l, i, "dataSystem")) { + // + // LDServerConfigBuilder_DataSystem_Enabled(builder, bool_or_default(l, i, "enabled", true)); + // + // if (has_table(l, i, "backgroundSync")) { + // + // require_string(l, i, "source", "dataSystem.backgroundSync.source"); + // + // const char *const source = luaL_checkstring(l, -1); + // if (strcmp(source, "streaming") == 0) { + // LDServerDataSourceStreamBuilder stream_builder = LDServerDataSourceStreamBuilder_New(); + // + // if (has_number(l, i, "initialReconnectDelayMs")) { + // LDServerDataSourceStreamBuilder_InitialReconnectDelayMs(stream_builder, luaL_checkinteger(l, -1)); + // } + // + // LDServerConfigBuilder_DataSystem_BackgroundSync_Streaming(builder, stream_builder); + // } else if (strcmp(source, "polling") == 0) { + // LDServerDataSourcePollBuilder poll_builder = LDServerDataSourcePollBuilder_New(); + // + // if (has_number(l, i, "intervalSeconds")) { + // LDServerDataSourcePollBuilder_IntervalS(poll_builder, luaL_checkinteger(l, -1)); + // } + // + // LDServerConfigBuilder_DataSystem_BackgroundSync_Polling(builder, poll_builder); + // } else { + // luaL_error(l, "dataSystem.backgroundSync.source must be 'streaming' or 'polling'"); + // } + // + // } else if (has_table(l, i, "lazyLoad")) { + // + // require_field(l, i, "source", "dataSystem.lazyLoad.source"); + // + // LDServerLazyLoadSourcePtr *source = luaL_checkudata(l, 1, "LaunchDarklyStoreInterface"); + // LDServerLazyLoadBuilder lazy_load_builder = LDServerLazyLoadBuilder_New(); + // LDServerLazyLoadBuilder_SourcePtr(lazy_load_builder, *source); + // + // } else { + // luaL_error(l, "dataSystem must have field backgroundSync or lazyLoad"); + // } + // } + // + // // TODO: Document change in behavior (offline now disables events + data system) + // LDServerConfigBuilder_Offline(builder, bool_or_default(l, i, "offline", false)); + // + // + // LDServerConfigBuilder_Events_AllAttributesPrivate(builder, bool_or_default(l, i, "allAttributesPrivate", false)); + // + // if (has_number(l, i, "contextKeysCapacity")) { + // // TODO: We don't have a C binding for this yet + // // LDServerConfigBuilder_Events_ContextKeysCapacity(builder, luaL_checkinteger(l, -1)); + // } + // + // if (has_table(l, i, "privateAttributeNames")) { + // int n = lua_tablelen(l, -1); + // + // for (int i = 1; i <= n; i++) { + // lua_rawgeti(l, -1, i); + // + // if (lua_isstring(l, -1)) { + // LDServerConfigBuilder_Events_PrivateAttribute(builder, luaL_checkstring(l, -1)); + // } else { + // luaL_error(l, "privateAttributeNames[%d] is not a string", i); + // } + // + // lua_pop(l, 1); + // } + // } if (globalLogEnabledCallback != LUA_NOREF && globalLogWriteCallback != LUA_NOREF) { struct LDLogBackend backend; @@ -619,9 +705,12 @@ makeConfig(lua_State *const l, const int i) LDServerConfig out_config; - LDServerConfigBuilder_Build(builder, &out_config); - - // TODO: Check result of the call + LDStatus status = LDServerConfigBuilder_Build(builder, &out_config); + if (!LDStatus_Ok(status)) { + lua_pushstring(l, LDStatus_Error(status)); + LDStatus_Free(status); + luaL_error(l, "SDK configuration invalid: %s", lua_tostring(l, -1)); + } return out_config; } @@ -692,10 +781,7 @@ LuaLDClientInit(lua_State *const l) return luaL_error(l, "expecting exactly 2 arguments"); } - config = makeConfig(l, 1); - - // TODO: use timeout - timeout = luaL_checkinteger(l, 2); + config = makeConfig(l); client = LDServerSDK_New(config); diff --git a/redis-test.lua b/redis-test.lua index 9d224f5..e3f9863 100644 --- a/redis-test.lua +++ b/redis-test.lua @@ -15,14 +15,15 @@ l.registerLogger(logWrite, logEnabled) TestAll = {} function makeTestClient() - local c = l.clientInit({ - key = "sdk-test", + local c = l.clientInit("sdk-test", { + baseURI = "foo", dataSystem = { + enabled = true, lazyLoad = { source = r.makeRedisSource('redis://localhost:1234', 'test-prefix') - } + }, } - }, 0) + }) return c end diff --git a/scripts/compile.sh b/scripts/compile.sh new file mode 100755 index 0000000..b9f12cd --- /dev/null +++ b/scripts/compile.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +set -e +luarocks make launchdarkly-server-sdk-1.0-0.rockspec LD_DIR=./cpp-sdks/build/INSTALL +luarocks make launchdarkly-server-sdk-redis-1.0-0.rockspec LDREDIS_DIR=./cpp-sdks/build/INSTALL diff --git a/scripts/build-cpp-server.sh b/scripts/get-cpp-sdk-locally.sh similarity index 59% rename from scripts/build-cpp-server.sh rename to scripts/get-cpp-sdk-locally.sh index b3e09cf..f6b0c55 100755 --- a/scripts/build-cpp-server.sh +++ b/scripts/get-cpp-sdk-locally.sh @@ -1,5 +1,11 @@ #!/bin/bash +# This is meant to be run when testing out changes locally without the help of CI. +# In CI, the C++ SDK's prebuilt Linux artifacts are fetched. +# +# Locally, it's more convenient to built the C++ SDK from source - to be able to switch branches, +# change built options, etc. + # Usage: ./scripts/build-cpp-server.sh set -e @@ -16,7 +22,7 @@ git clone --depth 1 --branch "$branch" https://github.com/launchdarkly/cpp-sdks. cd cpp-sdks mkdir build cd build -cmake -D LD_BUILD_SHARED_LIBS=ON \ +cmake -GNinja -D LD_BUILD_SHARED_LIBS=ON \ -D BUILD_TESTING=OFF \ -D LD_BUILD_EXAMPLES=OFF \ -D LD_BUILD_REDIS_SUPPORT=ON \ diff --git a/test.lua b/test.lua index 3b0255d..0b7a0a6 100644 --- a/test.lua +++ b/test.lua @@ -14,10 +14,9 @@ l.registerLogger(logWrite, logEnabled) TestAll = {} function makeTestClient() - local c = l.clientInit({ - key = "sdk-test", - offline = True - }, 0) + local c = l.clientInit("sdk-test", { + offline = true + }) return c end @@ -40,7 +39,7 @@ function TestAll:testBoolVariationDetail() value = true, reason = { kind = "ERROR", - errorKind = "CLIENT_NOT_READY", + errorKind = "FLAG_NOT_FOUND", inExperiment = false } } @@ -57,7 +56,7 @@ function TestAll:testIntVariationDetail() value = 5, reason = { kind = "ERROR", - errorKind = "CLIENT_NOT_READY", + errorKind = "FLAG_NOT_FOUND", inExperiment = false } } @@ -74,7 +73,7 @@ function TestAll:testDoubleVariationDetail() value = 6.2, reason = { kind = "ERROR", - errorKind = "CLIENT_NOT_READY", + errorKind = "FLAG_NOT_FOUND", inExperiment = false } } @@ -91,7 +90,7 @@ function TestAll:testStringVariationDetail() value = "f", reason = { kind = "ERROR", - errorKind = "CLIENT_NOT_READY", + errorKind = "FLAG_NOT_FOUND", inExperiment = false } } @@ -108,7 +107,7 @@ function TestAll:testJSONVariationDetail() value = { a = "b" }, reason = { kind = "ERROR", - errorKind = "CLIENT_NOT_READY", + errorKind = "FLAG_NOT_FOUND", inExperiment = false } } From 75be09e53aa65fddad91bd6fbd0a1bc5ef6a6f33 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Dec 2023 13:17:06 -0800 Subject: [PATCH 102/161] fix table-driven parser --- launchdarkly-server-sdk.c | 178 +++++++++++++++++++++++--------------- redis-test.lua | 1 - 2 files changed, 108 insertions(+), 71 deletions(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 378e1c4..ad05d9c 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -524,28 +524,115 @@ static void parse_number(lua_State *const l, int i, LDServerConfigBuilder builde setter(builder, value); } +struct config { + const char *name; + struct field_validator* fields; + int n; +}; + + +#define ARR_SIZE(x) (sizeof(x) / sizeof(x[0])) + +void traverse_config(lua_State *const l, int i, LDServerConfigBuilder builder, struct config *cfg); + +static void parse_table(lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data) { + lua_pushvalue(l, i); + traverse_config(l, i, builder, user_data); +} + +static void parse_lazyload_source(lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data) { + LDServerLazyLoadSourcePtr *source = luaL_checkudata(l, i, "LaunchDarklyStoreInterface"); + LDServerLazyLoadBuilder lazy_load_builder = LDServerLazyLoadBuilder_New(); + LDServerLazyLoadBuilder_SourcePtr(lazy_load_builder, *source); + + LDServerConfigBuilder_DataSystem_LazyLoad(builder, lazy_load_builder); +} + +struct field_validator lazyload_fields[] = { + {"source", LUA_TUSERDATA, parse_lazyload_source, NULL}, + /* TODO: Cache eviction TTL + policy */ +}; + +struct config lazyload_config = { + "dataSystem.lazyLoad", + lazyload_fields, + ARR_SIZE(lazyload_fields) +}; + + +struct field_validator streaming_fields[] = { + {"initialReconnectDelayMs", LUA_TNUMBER, parse_number, LDServerDataSourceStreamBuilder_InitialReconnectDelayMs}, +}; + +struct config streaming_config = { + "dataSystem.backgroundSync.streaming", + streaming_fields, + ARR_SIZE(streaming_fields) +}; + +struct field_validator polling_fields[] = { + {"intervalSeconds", LUA_TNUMBER, parse_number, LDServerDataSourcePollBuilder_IntervalS}, +}; + +struct config polling_config = { + "dataSystem.backgroundSync.polling", + polling_fields, + ARR_SIZE(polling_fields) +}; + +struct field_validator backgroundsync_fields[] = { + /* Mutually exclusive */ + {"streaming", LUA_TTABLE, parse_table, &streaming_config}, + {"polling", LUA_TTABLE, parse_table, &polling_config} +}; + +struct config backgroundsync_config = { + "dataSystem.backgroundSync", + backgroundsync_fields, + ARR_SIZE(backgroundsync_fields) +}; + +struct field_validator datasystem_fields[] = { + {"enabled", LUA_TBOOLEAN, parse_bool, LDServerConfigBuilder_DataSystem_Enabled}, + {"backgroundSync", LUA_TTABLE, parse_table, &backgroundsync_config}, + {"lazyLoad", LUA_TTABLE, parse_table, &lazyload_config}, +}; + +struct config datasystem_config = { + "dataSystem", + datasystem_fields, + ARR_SIZE(datasystem_fields) +}; + struct field_validator top_level_fields[] = { {"baseURI", LUA_TSTRING, parse_uri, LDServerConfigBuilder_ServiceEndpoints_PollingBaseURL}, {"streamURI", LUA_TSTRING, parse_uri, LDServerConfigBuilder_ServiceEndpoints_StreamingBaseURL}, {"eventsURI", LUA_TSTRING, parse_uri, LDServerConfigBuilder_ServiceEndpoints_EventsBaseURL}, {"offline", LUA_TBOOLEAN, parse_bool, LDServerConfigBuilder_Offline}, {"sendEvents", LUA_TBOOLEAN, parse_bool, LDServerConfigBuilder_Events_Enabled}, - {"contextKeysCapacity", LUA_TNUMBER, parse_number, NULL}, + {"contextKeysCapacity", LUA_TNUMBER, NULL, NULL}, {"allAttributesPrivate", LUA_TBOOLEAN, parse_bool, LDServerConfigBuilder_Events_AllAttributesPrivate}, {"eventsCapacity", LUA_TNUMBER, parse_number, LDServerConfigBuilder_Events_Capacity}, {"flushInterval", LUA_TNUMBER, parse_number, LDServerConfigBuilder_Events_FlushIntervalMs}, - {"privateAttributeNames", LUA_TTABLE}, - {"dataSystem", LUA_TTABLE} + {"privateAttributeNames", LUA_TTABLE, NULL, NULL}, + {"dataSystem", LUA_TTABLE, parse_table, &datasystem_config } }; +struct config top_level_config = { + "config", + top_level_fields, + ARR_SIZE(top_level_fields) +}; + + + // todo: appinfo -#define ARR_SIZE(x) (sizeof(x) / sizeof(x[0])) -struct field_validator * find_field(const char *key, struct field_validator *fields, int n) { - for (int i = 0; i < n; i++) { - if (strcmp(fields[i].key, key) == 0) { - return &fields[i]; +struct field_validator * find_field(const char *key, struct config* cfg) { + for (int i = 0; i < cfg->n; i++) { + if (strcmp(cfg->fields[i].key, key) == 0) { + return &cfg->fields[i]; } } return NULL; @@ -555,28 +642,28 @@ struct field_validator * find_field(const char *key, struct field_validator *fie #define debug_print(foo) do { if (DEBUG) printf(foo); } while (0) -void traverse_config(lua_State *const l, int i, LDServerConfigBuilder builder) { +void traverse_config(lua_State *const l, int i, LDServerConfigBuilder builder, struct config *cfg) { - luaL_checktype(l, i, LUA_TTABLE); + printf("traversing %s (%d fields)\n", cfg->name, cfg->n); + luaL_checktype(l, -1, LUA_TTABLE); lua_pushnil(l); - while (lua_next(l, -2)) { + while (lua_next(l, -2) != 0) { lua_pushvalue(l, -2); - const char* key = lua_tostring(l, -1); int type = lua_type(l, -2); - struct field_validator *field = find_field(key, top_level_fields, ARR_SIZE(top_level_fields)); + struct field_validator *field = find_field(key, cfg); if (field == NULL) { - luaL_error(l, "unrecognized config field: %s", key); + luaL_error(l, "unrecognized %s field: %s", cfg->name, key); } if (field->type != type) { - luaL_error(l, "config field %s must be a %s", key, lua_typename(l, field->type)); + luaL_error(l, "%s field %s must be a %s", cfg->name, key, lua_typename(l, field->type)); } - if (field->parse) { + if (field->parse != NULL) { field->parse(l, -2, builder, field->user_data); } else { - luaL_error(l, "missing field parser for %s", key); + luaL_error(l, "%s missing field parser for %s", cfg->name, key); } lua_pop(l, 2); } @@ -591,38 +678,10 @@ makeConfig(lua_State *const l) const char* sdk_key = luaL_checkstring(l, 1); LDServerConfigBuilder builder = LDServerConfigBuilder_New(sdk_key); - traverse_config(l, 2, builder); + traverse_config(l, 2, builder, &top_level_config); + - // luaL_error(l, "nope"); - // // Base, stream, and eventsURIs are optional - but if one is set, all - // // must be set (enforced by C++ SDK.) - // if (has_string(l, i, "baseURI")) { - // LDServerConfigBuilder_ServiceEndpoints_PollingBaseURL(builder, luaL_checkstring(l, -1)); - // } - // if (has_string(l, i, "streamURI")) { - // LDServerConfigBuilder_ServiceEndpoints_StreamingBaseURL(builder, luaL_checkstring(l, -1)); - // } - // if (has_string(l, i, "eventsURI")) { - // LDServerConfigBuilder_ServiceEndpoints_EventsBaseURL(builder, luaL_checkstring(l, -1)); - // } - // - // - // LDServerConfigBuilder_Events_Enabled(builder, bool_or_default(l, i, "sendEvents", true)); - // - // if (has_number(l, i, "eventsCapacity")) { - // LDServerConfigBuilder_Events_Capacity(builder, luaL_checkinteger(l, -1)); - // } - // if (has_number(l, i, "flushInterval")) { - // LDServerConfigBuilder_Events_FlushIntervalMs(builder, luaL_checkinteger(l, -1)); - // } - // - // - // - // if (has_table(l, i, "dataSystem")) { - // - // LDServerConfigBuilder_DataSystem_Enabled(builder, bool_or_default(l, i, "enabled", true)); - // // if (has_table(l, i, "backgroundSync")) { // // require_string(l, i, "source", "dataSystem.backgroundSync.source"); @@ -648,29 +707,8 @@ makeConfig(lua_State *const l) // luaL_error(l, "dataSystem.backgroundSync.source must be 'streaming' or 'polling'"); // } // - // } else if (has_table(l, i, "lazyLoad")) { - // - // require_field(l, i, "source", "dataSystem.lazyLoad.source"); - // - // LDServerLazyLoadSourcePtr *source = luaL_checkudata(l, 1, "LaunchDarklyStoreInterface"); - // LDServerLazyLoadBuilder lazy_load_builder = LDServerLazyLoadBuilder_New(); - // LDServerLazyLoadBuilder_SourcePtr(lazy_load_builder, *source); - // - // } else { - // luaL_error(l, "dataSystem must have field backgroundSync or lazyLoad"); - // } - // } - // - // // TODO: Document change in behavior (offline now disables events + data system) - // LDServerConfigBuilder_Offline(builder, bool_or_default(l, i, "offline", false)); - // - // - // LDServerConfigBuilder_Events_AllAttributesPrivate(builder, bool_or_default(l, i, "allAttributesPrivate", false)); - // - // if (has_number(l, i, "contextKeysCapacity")) { - // // TODO: We don't have a C binding for this yet - // // LDServerConfigBuilder_Events_ContextKeysCapacity(builder, luaL_checkinteger(l, -1)); - // } + + // // if (has_table(l, i, "privateAttributeNames")) { // int n = lua_tablelen(l, -1); diff --git a/redis-test.lua b/redis-test.lua index e3f9863..20fb97d 100644 --- a/redis-test.lua +++ b/redis-test.lua @@ -16,7 +16,6 @@ TestAll = {} function makeTestClient() local c = l.clientInit("sdk-test", { - baseURI = "foo", dataSystem = { enabled = true, lazyLoad = { From 9efa84f551f3c87ad0f52899de2bd4e2c5e7debc Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Dec 2023 14:11:23 -0800 Subject: [PATCH 103/161] fix bug in parsers --- launchdarkly-server-sdk.c | 246 ++++++++++++-------------------------- test.lua | 29 +++++ 2 files changed, 105 insertions(+), 170 deletions(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index ad05d9c..4f49a45 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -413,126 +413,49 @@ LuaLDUserFree(lua_State *const l) return 0; } -static void debug(lua_State *const l, const int i){ - int t = lua_type(l, -1); - switch (t) { - - case LUA_TSTRING: /* strings */ - printf("`%s'", lua_tostring(l, i)); - break; - - case LUA_TBOOLEAN: /* booleans */ - printf(lua_toboolean(l, i) ? "true" : "false"); - break; - - case LUA_TNUMBER: /* numbers */ - printf("%g", lua_tonumber(l, i)); - break; - - default: /* other values */ - printf("%s", lua_typename(l, t)); - break; - - } - printf("\n"); -} -static bool has_table(lua_State *const l, const int i, const char* name) { - lua_getfield(l, i, name); - debug(l, i); - if (lua_istable(l, -1)) { - return true; - } - lua_pop(l, 1); - return false; -} - -static bool has_string(lua_State *const l, const int i, const char* name) { - lua_getfield(l, i, name); - if (lua_isstring(l, -1)) { - return true; - } - lua_pop(l, 1); - return false; -} - -static void require_string(lua_State *const l, const int i, const char* name, const char* full_path) { - lua_getfield(l, i, name); - if (!lua_isstring(l, -1)) { - luaL_error(l, "%s must be provided as a string", full_path ? full_path : name); - } -} - -static void require_field(lua_State *const l, const int i, const char* name, const char* full_path) { - lua_getfield(l, i, name); - if (lua_isnil(l, -1)) { - luaL_error(l, "%s must be provided", full_path ? full_path : name); - } -} - -static void dump_table(lua_State *const l, const int i) { - lua_pushnil(l); - while (lua_next(l, i) != 0) { - printf("%s: %s\n", - luaL_checkstring(l, -2), - lua_typename(l, lua_type(l, -1))); - lua_pop(l, 1); - } -} - -static bool has_number(lua_State *const l, const int i, const char* name) { - lua_getfield(l, i, name); - if (lua_isnumber(l, -1)) { - return true; - } - lua_pop(l, 1); - return false; -} - -static bool bool_or_default(lua_State *const l, const int i, const char* name, const bool default_value) { - bool result = default_value; - lua_getfield(l, i, name); - if (lua_isboolean(l, -1)) { - result = lua_toboolean(l, -1); - } - lua_pop(l, 1); - return result; -} - - +// field_validator is used to validate a single field in a config table. struct field_validator { + // Name of the field. const char* key; + // Expected Lua type of the field. int type; + // Function pointer to parse the field. If NULL, an error will be reported + // at runtime if the field exists. The 'user_data' field is passed in. void (*parse) (lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data); + // Store a pointer to arbitrary data for use in 'parse'. void *user_data; }; -static void parse_uri(lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data) { - const char *const uri = luaL_checkstring(l, -1); +// Parses a string from the top of the stack and then calls a setter function +// stored in user_data. The setter must have the signature (LDServerConfigBuilder, const char*). +static void parse_string(lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data) { + const char *const uri = luaL_checkstring(l, i); void (*setter)(LDServerConfigBuilder, const char*) = user_data; setter(builder, uri); } +// Parses a bool from the top of the stack and then calls a setter function +// stored in user_data. The setter must have the signature (LDServerConfigBuilder, bool). static void parse_bool(lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data) { - const bool value = lua_toboolean(l, -1); + const bool value = lua_toboolean(l, i); void (*setter)(LDServerConfigBuilder, bool) = user_data; setter(builder, value); } +// Parses a number from the top of the stack and then calls a setter function +// stored in user_data. The setter must have the signature (LDServerConfigBuilder, int). static void parse_number(lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data) { - const int value = lua_tointeger(l, -1); + const int value = lua_tointeger(l, i); void (*setter)(LDServerConfigBuilder, int) = user_data; setter(builder, value); } -struct config { - const char *name; - struct field_validator* fields; - int n; -}; - - -#define ARR_SIZE(x) (sizeof(x) / sizeof(x[0])) +// Forward declaration of the config used in traverse_config, to keep parse_table +// in the same place as the others. +struct config; +// Forward declaration for same reason as above. Traverses a config recursively, +// calling the appropriate parse function for each field. void traverse_config(lua_State *const l, int i, LDServerConfigBuilder builder, struct config *cfg); static void parse_table(lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data) { @@ -540,6 +463,7 @@ static void parse_table(lua_State *const l, int i, LDServerConfigBuilder builder traverse_config(l, i, builder, user_data); } +// Special purpose parser for grabbing a store interface from a userdata. static void parse_lazyload_source(lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data) { LDServerLazyLoadSourcePtr *source = luaL_checkudata(l, i, "LaunchDarklyStoreInterface"); LDServerLazyLoadBuilder lazy_load_builder = LDServerLazyLoadBuilder_New(); @@ -548,37 +472,39 @@ static void parse_lazyload_source(lua_State *const l, int i, LDServerConfigBuild LDServerConfigBuilder_DataSystem_LazyLoad(builder, lazy_load_builder); } -struct field_validator lazyload_fields[] = { - {"source", LUA_TUSERDATA, parse_lazyload_source, NULL}, - /* TODO: Cache eviction TTL + policy */ +// Stores a list of fields and the field count. The name is used for error reporting. +struct config { + const char *name; + struct field_validator* fields; + int n; }; -struct config lazyload_config = { - "dataSystem.lazyLoad", - lazyload_fields, - ARR_SIZE(lazyload_fields) +#define ARR_SIZE(x) (sizeof(x) / sizeof(x[0])) + +// Use this macro to define new config tables. Config tables can be nested arbitrarily. +#define DEFINE_CONFIG(name, path, fields) \ + struct config name = {#path, fields, ARR_SIZE(fields)} + + +struct field_validator lazyload_fields[] = { + {"source", LUA_TUSERDATA, parse_lazyload_source, NULL /* not needed */}, + {"cacheRefreshMilliseconds", LUA_TNUMBER, parse_number, LDServerLazyLoadBuilder_CacheRefreshMs}, + {"cacheEvictionPolicy", LUA_TNUMBER, parse_number, LDServerLazyLoadBuilder_CachePolicy} }; +DEFINE_CONFIG(lazyload_config, "dataSystem.lazyLoad", lazyload_fields); struct field_validator streaming_fields[] = { - {"initialReconnectDelayMs", LUA_TNUMBER, parse_number, LDServerDataSourceStreamBuilder_InitialReconnectDelayMs}, + {"initialReconnectDelayMilliseconds", LUA_TNUMBER, parse_number, LDServerDataSourceStreamBuilder_InitialReconnectDelayMs}, }; -struct config streaming_config = { - "dataSystem.backgroundSync.streaming", - streaming_fields, - ARR_SIZE(streaming_fields) -}; +DEFINE_CONFIG(streaming_config, "dataSystem.backgroundSync.streaming", streaming_fields); struct field_validator polling_fields[] = { {"intervalSeconds", LUA_TNUMBER, parse_number, LDServerDataSourcePollBuilder_IntervalS}, }; -struct config polling_config = { - "dataSystem.backgroundSync.polling", - polling_fields, - ARR_SIZE(polling_fields) -}; +DEFINE_CONFIG(polling_config, "dataSystem.backgroundSync.polling", polling_fields); struct field_validator backgroundsync_fields[] = { /* Mutually exclusive */ @@ -586,44 +512,53 @@ struct field_validator backgroundsync_fields[] = { {"polling", LUA_TTABLE, parse_table, &polling_config} }; -struct config backgroundsync_config = { - "dataSystem.backgroundSync", - backgroundsync_fields, - ARR_SIZE(backgroundsync_fields) -}; + +DEFINE_CONFIG(backgroundsync_config, "dataSystem.backgroundSync", backgroundsync_fields); struct field_validator datasystem_fields[] = { {"enabled", LUA_TBOOLEAN, parse_bool, LDServerConfigBuilder_DataSystem_Enabled}, {"backgroundSync", LUA_TTABLE, parse_table, &backgroundsync_config}, - {"lazyLoad", LUA_TTABLE, parse_table, &lazyload_config}, + {"lazyLoad", LUA_TTABLE, parse_table, &lazyload_config} }; -struct config datasystem_config = { - "dataSystem", - datasystem_fields, - ARR_SIZE(datasystem_fields) -}; +DEFINE_CONFIG(datasystem_config, "dataSystem", datasystem_fields); -struct field_validator top_level_fields[] = { - {"baseURI", LUA_TSTRING, parse_uri, LDServerConfigBuilder_ServiceEndpoints_PollingBaseURL}, - {"streamURI", LUA_TSTRING, parse_uri, LDServerConfigBuilder_ServiceEndpoints_StreamingBaseURL}, - {"eventsURI", LUA_TSTRING, parse_uri, LDServerConfigBuilder_ServiceEndpoints_EventsBaseURL}, - {"offline", LUA_TBOOLEAN, parse_bool, LDServerConfigBuilder_Offline}, - {"sendEvents", LUA_TBOOLEAN, parse_bool, LDServerConfigBuilder_Events_Enabled}, +struct field_validator event_fields[] = { + {"enabled", LUA_TBOOLEAN, parse_bool, LDServerConfigBuilder_Events_Enabled}, {"contextKeysCapacity", LUA_TNUMBER, NULL, NULL}, + {"capacity", LUA_TNUMBER, parse_number, LDServerConfigBuilder_Events_Capacity}, + {"flushIntervalMilliseconds", LUA_TNUMBER, parse_number, LDServerConfigBuilder_Events_FlushIntervalMs}, {"allAttributesPrivate", LUA_TBOOLEAN, parse_bool, LDServerConfigBuilder_Events_AllAttributesPrivate}, - {"eventsCapacity", LUA_TNUMBER, parse_number, LDServerConfigBuilder_Events_Capacity}, - {"flushInterval", LUA_TNUMBER, parse_number, LDServerConfigBuilder_Events_FlushIntervalMs}, - {"privateAttributeNames", LUA_TTABLE, NULL, NULL}, - {"dataSystem", LUA_TTABLE, parse_table, &datasystem_config } + {"privateAttributeNames", LUA_TTABLE, NULL, NULL} /* TODO */ }; -struct config top_level_config = { - "config", - top_level_fields, - ARR_SIZE(top_level_fields) +DEFINE_CONFIG(event_config, "events", event_fields); + +struct field_validator endpoint_fields[] = { + {"pollingBaseURL", LUA_TSTRING, parse_string, LDServerConfigBuilder_ServiceEndpoints_PollingBaseURL}, + {"streamingBaseURL", LUA_TSTRING, parse_string, LDServerConfigBuilder_ServiceEndpoints_StreamingBaseURL}, + {"eventsBaseURL", LUA_TSTRING, parse_string, LDServerConfigBuilder_ServiceEndpoints_EventsBaseURL} }; +DEFINE_CONFIG(endpoint_config, "serviceEndpoints", endpoint_fields); + +struct field_validator appinfo_fields[] = { + {"identifier", LUA_TSTRING, parse_string, LDServerConfigBuilder_AppInfo_Identifier}, + {"version", LUA_TSTRING, parse_string, LDServerConfigBuilder_AppInfo_Version} +}; + +DEFINE_CONFIG(appinfo_config, "appInfo", appinfo_fields); + +struct field_validator top_level_fields[] = { + {"appInfo", LUA_TTABLE, parse_table, &appinfo_config}, + {"serviceEndpoints", LUA_TTABLE, parse_table, &endpoint_config}, + {"offline", LUA_TBOOLEAN, parse_bool, LDServerConfigBuilder_Offline}, + {"dataSystem", LUA_TTABLE, parse_table, &datasystem_config }, + {"events", LUA_TTABLE, parse_table, &event_config } +}; + + +DEFINE_CONFIG(top_level_config, "config", top_level_fields); // todo: appinfo @@ -643,8 +578,6 @@ struct field_validator * find_field(const char *key, struct config* cfg) { #define debug_print(foo) do { if (DEBUG) printf(foo); } while (0) void traverse_config(lua_State *const l, int i, LDServerConfigBuilder builder, struct config *cfg) { - - printf("traversing %s (%d fields)\n", cfg->name, cfg->n); luaL_checktype(l, -1, LUA_TTABLE); lua_pushnil(l); @@ -682,33 +615,6 @@ makeConfig(lua_State *const l) - // if (has_table(l, i, "backgroundSync")) { - // - // require_string(l, i, "source", "dataSystem.backgroundSync.source"); - // - // const char *const source = luaL_checkstring(l, -1); - // if (strcmp(source, "streaming") == 0) { - // LDServerDataSourceStreamBuilder stream_builder = LDServerDataSourceStreamBuilder_New(); - // - // if (has_number(l, i, "initialReconnectDelayMs")) { - // LDServerDataSourceStreamBuilder_InitialReconnectDelayMs(stream_builder, luaL_checkinteger(l, -1)); - // } - // - // LDServerConfigBuilder_DataSystem_BackgroundSync_Streaming(builder, stream_builder); - // } else if (strcmp(source, "polling") == 0) { - // LDServerDataSourcePollBuilder poll_builder = LDServerDataSourcePollBuilder_New(); - // - // if (has_number(l, i, "intervalSeconds")) { - // LDServerDataSourcePollBuilder_IntervalS(poll_builder, luaL_checkinteger(l, -1)); - // } - // - // LDServerConfigBuilder_DataSystem_BackgroundSync_Polling(builder, poll_builder); - // } else { - // luaL_error(l, "dataSystem.backgroundSync.source must be 'streaming' or 'polling'"); - // } - // - - // // if (has_table(l, i, "privateAttributeNames")) { // int n = lua_tablelen(l, -1); diff --git a/test.lua b/test.lua index 0b7a0a6..ad45f8b 100644 --- a/test.lua +++ b/test.lua @@ -29,6 +29,35 @@ function TestAll:tearDown() collectgarbage("collect") end +function TestAll:testSetAllConfigFields() + local c = l.clientInit("sdk-test", { + offline = true, + appInfo = { + identifier = "MyApp", + version = "1.0.0" + }, + serviceEndpoints = { + streamingBaseURL = "foo", + pollingBaseURL = "bar", + eventsBaseURL = "baz" + }, + dataSystem = { + backgroundSync = { + streaming = { + initialReconnectDelayMilliseconds = 1000 + } + } + }, + events = { + capacity = 1000, + --contextKeysCapacity = 100, TODO: once c binding available + enabled = true, + flushIntervalMilliseconds = 100, + allAttributesPrivate = true + } + }) +end + function TestAll:testBoolVariation() local e = false u.assertEquals(makeTestClient():boolVariation(user, "test", e), e) From 63552f45385c8b7c2fd27f5380c343916f5a33c7 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Dec 2023 14:32:38 -0800 Subject: [PATCH 104/161] cleanup --- launchdarkly-server-sdk.c | 28 +++++++++++----------------- redis-test.lua | 1 - scripts/compile.sh | 3 +++ scripts/get-cpp-sdk-locally.sh | 7 +++++-- test.lua | 2 +- 5 files changed, 20 insertions(+), 21 deletions(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 4f49a45..ce78139 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -557,25 +557,10 @@ struct field_validator top_level_fields[] = { {"events", LUA_TTABLE, parse_table, &event_config } }; - DEFINE_CONFIG(top_level_config, "config", top_level_fields); - -// todo: appinfo - - -struct field_validator * find_field(const char *key, struct config* cfg) { - for (int i = 0; i < cfg->n; i++) { - if (strcmp(cfg->fields[i].key, key) == 0) { - return &cfg->fields[i]; - } - } - return NULL; -} - -#define DEBUG 1 - -#define debug_print(foo) do { if (DEBUG) printf(foo); } while (0) +// Finds a field by key in the given config, or returns NULL. +struct field_validator * find_field(const char *key, struct config* cfg); void traverse_config(lua_State *const l, int i, LDServerConfigBuilder builder, struct config *cfg) { luaL_checktype(l, -1, LUA_TTABLE); @@ -603,6 +588,15 @@ void traverse_config(lua_State *const l, int i, LDServerConfigBuilder builder, s lua_pop(l, 1); } +struct field_validator * find_field(const char *key, struct config* cfg) { + for (int i = 0; i < cfg->n; i++) { + if (strcmp(cfg->fields[i].key, key) == 0) { + return &cfg->fields[i]; + } + } + return NULL; +} + static LDServerConfig makeConfig(lua_State *const l) { diff --git a/redis-test.lua b/redis-test.lua index 20fb97d..599d859 100644 --- a/redis-test.lua +++ b/redis-test.lua @@ -23,7 +23,6 @@ function makeTestClient() }, } }) - return c end diff --git a/scripts/compile.sh b/scripts/compile.sh index b9f12cd..d5fe35c 100755 --- a/scripts/compile.sh +++ b/scripts/compile.sh @@ -1,5 +1,8 @@ #!/bin/sh +# Convenience to compile both the normal SDK and Redis luarocks at once, using +# the paths fetched by ./get-cpp-sdk-locally.sh. + set -e luarocks make launchdarkly-server-sdk-1.0-0.rockspec LD_DIR=./cpp-sdks/build/INSTALL luarocks make launchdarkly-server-sdk-redis-1.0-0.rockspec LDREDIS_DIR=./cpp-sdks/build/INSTALL diff --git a/scripts/get-cpp-sdk-locally.sh b/scripts/get-cpp-sdk-locally.sh index f6b0c55..45f054f 100755 --- a/scripts/get-cpp-sdk-locally.sh +++ b/scripts/get-cpp-sdk-locally.sh @@ -1,12 +1,15 @@ #!/bin/bash -# This is meant to be run when testing out changes locally without the help of CI. -# In CI, the C++ SDK's prebuilt Linux artifacts are fetched. +# This is meant to be run when testing out changes locally. In contrast, the remote CI +# fetches prebuilt artifacts from github based on a release tag. # # Locally, it's more convenient to built the C++ SDK from source - to be able to switch branches, # change built options, etc. # Usage: ./scripts/build-cpp-server.sh +# If no tag is supplied, it uses 'main'. +# +# The SDK headers/libs are installed in ./cpp-sdks/build/INSTALL. set -e diff --git a/test.lua b/test.lua index ad45f8b..0961dfe 100644 --- a/test.lua +++ b/test.lua @@ -50,7 +50,7 @@ function TestAll:testSetAllConfigFields() }, events = { capacity = 1000, - --contextKeysCapacity = 100, TODO: once c binding available + --contextKeysCapacity = 100, TODO: add once c binding available enabled = true, flushIntervalMilliseconds = 100, allAttributesPrivate = true From 59eac4a5db637b2e252bb717640e9d21746e521a Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Dec 2023 14:37:21 -0800 Subject: [PATCH 105/161] update gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 485dee6..fe99dac 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ .idea +cpp-sdks +*.o +*.so From 1c86f2025f6f14d826068c1e3180042f90bc5230 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Dec 2023 15:05:13 -0800 Subject: [PATCH 106/161] add privateAttributes support --- launchdarkly-server-sdk.c | 75 +++++++++++++++++++++------------------ test.lua | 3 +- 2 files changed, 43 insertions(+), 35 deletions(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index ce78139..0391195 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -426,24 +426,24 @@ struct field_validator { void *user_data; }; -// Parses a string from the top of the stack and then calls a setter function -// stored in user_data. The setter must have the signature (LDServerConfigBuilder, const char*). +// Parses a string and then calls a setter function stored in user_data. +// The setter must have the signature (LDServerConfigBuilder, const char*). static void parse_string(lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data) { const char *const uri = luaL_checkstring(l, i); void (*setter)(LDServerConfigBuilder, const char*) = user_data; setter(builder, uri); } -// Parses a bool from the top of the stack and then calls a setter function -// stored in user_data. The setter must have the signature (LDServerConfigBuilder, bool). +// Parses a bool and then calls a setter function stored in user_data. +// The setter must have the signature (LDServerConfigBuilder, bool). static void parse_bool(lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data) { const bool value = lua_toboolean(l, i); void (*setter)(LDServerConfigBuilder, bool) = user_data; setter(builder, value); } -// Parses a number from the top of the stack and then calls a setter function -// stored in user_data. The setter must have the signature (LDServerConfigBuilder, int). +// Parses a number and then calls a setter function stored in user_data. +// The setter must have the signature (LDServerConfigBuilder, int). static void parse_number(lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data) { const int value = lua_tointeger(l, i); void (*setter)(LDServerConfigBuilder, int) = user_data; @@ -455,12 +455,35 @@ static void parse_number(lua_State *const l, int i, LDServerConfigBuilder builde struct config; // Forward declaration for same reason as above. Traverses a config recursively, -// calling the appropriate parse function for each field. -void traverse_config(lua_State *const l, int i, LDServerConfigBuilder builder, struct config *cfg); +// calling the appropriate parse function for each field. It expects a table to be +// on top of the stack. +void traverse_config(lua_State *const l, LDServerConfigBuilder builder, struct config *cfg); +// Parses a table using traverse_config. static void parse_table(lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data) { + // since traverse_config expects the table to be on top of the stack, + // make it so. lua_pushvalue(l, i); - traverse_config(l, i, builder, user_data); + traverse_config(l, builder, user_data); +} + +// Parses an array of strings. Items that aren't strings are silently ignored. +static void parse_string_array(lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data) { + void (*setter)(LDServerConfigBuilder, const char*) = user_data; + + lua_pushvalue(l, i); + + int n = lua_tablelen(l, -1); + + for (int i = 1; i <= n; i++) { + lua_rawgeti(l, -1, i); + if (lua_isstring(l, -1)) { + setter(builder, luaL_checkstring(l, -1)); + } + lua_pop(l, 1); + } + + lua_pop(l, 1); } // Special purpose parser for grabbing a store interface from a userdata. @@ -529,7 +552,7 @@ struct field_validator event_fields[] = { {"capacity", LUA_TNUMBER, parse_number, LDServerConfigBuilder_Events_Capacity}, {"flushIntervalMilliseconds", LUA_TNUMBER, parse_number, LDServerConfigBuilder_Events_FlushIntervalMs}, {"allAttributesPrivate", LUA_TBOOLEAN, parse_bool, LDServerConfigBuilder_Events_AllAttributesPrivate}, - {"privateAttributeNames", LUA_TTABLE, NULL, NULL} /* TODO */ + {"privateAttributes", LUA_TTABLE, parse_string_array, LDServerConfigBuilder_Events_PrivateAttribute} }; DEFINE_CONFIG(event_config, "events", event_fields); @@ -562,9 +585,8 @@ DEFINE_CONFIG(top_level_config, "config", top_level_fields); // Finds a field by key in the given config, or returns NULL. struct field_validator * find_field(const char *key, struct config* cfg); -void traverse_config(lua_State *const l, int i, LDServerConfigBuilder builder, struct config *cfg) { +void traverse_config(lua_State *const l, LDServerConfigBuilder builder, struct config *cfg) { luaL_checktype(l, -1, LUA_TTABLE); - lua_pushnil(l); while (lua_next(l, -2) != 0) { lua_pushvalue(l, -2); @@ -600,31 +622,16 @@ struct field_validator * find_field(const char *key, struct config* cfg) { static LDServerConfig makeConfig(lua_State *const l) { - // We are passed two arguments. One string (sdk key), followed by - // a table of config options. + // We have been passed two arguments: + // First: the SDK key (string) + // Second: the config structure (table) const char* sdk_key = luaL_checkstring(l, 1); LDServerConfigBuilder builder = LDServerConfigBuilder_New(sdk_key); - traverse_config(l, 2, builder, &top_level_config); - - - - // - // if (has_table(l, i, "privateAttributeNames")) { - // int n = lua_tablelen(l, -1); - // - // for (int i = 1; i <= n; i++) { - // lua_rawgeti(l, -1, i); - // - // if (lua_isstring(l, -1)) { - // LDServerConfigBuilder_Events_PrivateAttribute(builder, luaL_checkstring(l, -1)); - // } else { - // luaL_error(l, "privateAttributeNames[%d] is not a string", i); - // } - // - // lua_pop(l, 1); - // } - // } + + // Recursively visit the heirarchical configs, modifying the builder + // as we go along. + traverse_config(l, builder, &top_level_config); if (globalLogEnabledCallback != LUA_NOREF && globalLogWriteCallback != LUA_NOREF) { struct LDLogBackend backend; diff --git a/test.lua b/test.lua index 0961dfe..a41709c 100644 --- a/test.lua +++ b/test.lua @@ -53,7 +53,8 @@ function TestAll:testSetAllConfigFields() --contextKeysCapacity = 100, TODO: add once c binding available enabled = true, flushIntervalMilliseconds = 100, - allAttributesPrivate = true + allAttributesPrivate = true, + privateAttributes = {"/foo", "/bar"} } }) end From 0a2cb205fb41fe8668a86043915c6c7af7c3a87b Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Dec 2023 15:29:58 -0800 Subject: [PATCH 107/161] will it segfault? --- launchdarkly-server-sdk.c | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 0391195..496086e 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -419,8 +419,8 @@ struct field_validator { const char* key; // Expected Lua type of the field. int type; - // Function pointer to parse the field. If NULL, an error will be reported - // at runtime if the field exists. The 'user_data' field is passed in. + // Function to parse the value on the top of the stack. If NULL, an error will be reported + // at runtime if the field exists. void (*parse) (lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data); // Store a pointer to arbitrary data for use in 'parse'. void *user_data; @@ -429,7 +429,7 @@ struct field_validator { // Parses a string and then calls a setter function stored in user_data. // The setter must have the signature (LDServerConfigBuilder, const char*). static void parse_string(lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data) { - const char *const uri = luaL_checkstring(l, i); + const char *const uri = lua_tostring(l, i); void (*setter)(LDServerConfigBuilder, const char*) = user_data; setter(builder, uri); } @@ -460,7 +460,7 @@ struct config; void traverse_config(lua_State *const l, LDServerConfigBuilder builder, struct config *cfg); // Parses a table using traverse_config. -static void parse_table(lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data) { +static void parse_table(lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data) { // since traverse_config expects the table to be on top of the stack, // make it so. lua_pushvalue(l, i); @@ -471,23 +471,21 @@ static void parse_table(lua_State *const l, int i, LDServerConfigBuilder builder static void parse_string_array(lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data) { void (*setter)(LDServerConfigBuilder, const char*) = user_data; - lua_pushvalue(l, i); - int n = lua_tablelen(l, -1); for (int i = 1; i <= n; i++) { lua_rawgeti(l, -1, i); if (lua_isstring(l, -1)) { - setter(builder, luaL_checkstring(l, -1)); + setter(builder, lua_tostring(l, -1)); } lua_pop(l, 1); } - lua_pop(l, 1); } // Special purpose parser for grabbing a store interface from a userdata. static void parse_lazyload_source(lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data) { +// TODO: replace checkudata LDServerLazyLoadSourcePtr *source = luaL_checkudata(l, i, "LaunchDarklyStoreInterface"); LDServerLazyLoadBuilder lazy_load_builder = LDServerLazyLoadBuilder_New(); LDServerLazyLoadBuilder_SourcePtr(lazy_load_builder, *source); @@ -586,7 +584,11 @@ DEFINE_CONFIG(top_level_config, "config", top_level_fields); struct field_validator * find_field(const char *key, struct config* cfg); void traverse_config(lua_State *const l, LDServerConfigBuilder builder, struct config *cfg) { - luaL_checktype(l, -1, LUA_TTABLE); + + printf("traverse_config: %s\n", cfg->name); + if (lua_type(l, -1) != LUA_TTABLE) { + luaL_error(l, "%s must be a table", cfg->name); + } lua_pushnil(l); while (lua_next(l, -2) != 0) { lua_pushvalue(l, -2); @@ -601,6 +603,7 @@ void traverse_config(lua_State *const l, LDServerConfigBuilder builder, struct c luaL_error(l, "%s field %s must be a %s", cfg->name, key, lua_typename(l, field->type)); } if (field->parse != NULL) { + printf("parsing %s\n", key); field->parse(l, -2, builder, field->user_data); } else { luaL_error(l, "%s missing field parser for %s", cfg->name, key); From e5eb9f422400214873c73fd04664c6d28fffcac8 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Dec 2023 15:39:08 -0800 Subject: [PATCH 108/161] try again --- launchdarkly-server-sdk.c | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 496086e..5de5c37 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -471,22 +471,21 @@ static void parse_table(lua_State *const l, int i, LDServerConfigBuilder builde static void parse_string_array(lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data) { void (*setter)(LDServerConfigBuilder, const char*) = user_data; - int n = lua_tablelen(l, -1); + int n = lua_tablelen(l, i); - for (int i = 1; i <= n; i++) { - lua_rawgeti(l, -1, i); + for (int j = 1; j <= n; j++) { + lua_rawgeti(l, i, j); if (lua_isstring(l, -1)) { setter(builder, lua_tostring(l, -1)); } lua_pop(l, 1); } - } // Special purpose parser for grabbing a store interface from a userdata. static void parse_lazyload_source(lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data) { // TODO: replace checkudata - LDServerLazyLoadSourcePtr *source = luaL_checkudata(l, i, "LaunchDarklyStoreInterface"); + LDServerLazyLoadSourcePtr *source = lua_touserdata(l, i); LDServerLazyLoadBuilder lazy_load_builder = LDServerLazyLoadBuilder_New(); LDServerLazyLoadBuilder_SourcePtr(lazy_load_builder, *source); From 8b6a6100e9cbdb0f52da6b930a1d1cd1fe9311f9 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Dec 2023 15:51:08 -0800 Subject: [PATCH 109/161] more logging --- launchdarkly-server-sdk.c | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 5de5c37..cc3a882 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -19,7 +19,13 @@ Server-side SDK for LaunchDarkly. #include #include +#define DEBUG 1 +#ifdef DEBUG +#define DEBUG_PRINT(fmt, ...) printf(fmt, __VA_ARGS__) +#else +#define DEBUG_PRINT(fmt, ...) +#endif #define SDKVersion "1.2.2" /* {x-release-please-version} */ @@ -467,16 +473,20 @@ static void parse_table(lua_State *const l, int i, LDServerConfigBuilder builde traverse_config(l, builder, user_data); } + + // Parses an array of strings. Items that aren't strings are silently ignored. static void parse_string_array(lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data) { void (*setter)(LDServerConfigBuilder, const char*) = user_data; - int n = lua_tablelen(l, i); + DEBUG_PRINT("parsing string array of length %d\n", n); for (int j = 1; j <= n; j++) { lua_rawgeti(l, i, j); if (lua_isstring(l, -1)) { - setter(builder, lua_tostring(l, -1)); + const char* elem = lua_tostring(l, -1); + DEBUG_PRINT("array[%d] = %s\n", j, elem); + setter(builder, elem); } lua_pop(l, 1); } @@ -503,7 +513,7 @@ struct config { // Use this macro to define new config tables. Config tables can be nested arbitrarily. #define DEFINE_CONFIG(name, path, fields) \ - struct config name = {#path, fields, ARR_SIZE(fields)} + struct config name = {path, fields, ARR_SIZE(fields)} struct field_validator lazyload_fields[] = { @@ -583,8 +593,7 @@ DEFINE_CONFIG(top_level_config, "config", top_level_fields); struct field_validator * find_field(const char *key, struct config* cfg); void traverse_config(lua_State *const l, LDServerConfigBuilder builder, struct config *cfg) { - - printf("traverse_config: %s\n", cfg->name); + DEBUG_PRINT("traversing %s\n", cfg->name); if (lua_type(l, -1) != LUA_TTABLE) { luaL_error(l, "%s must be a table", cfg->name); } @@ -594,6 +603,8 @@ void traverse_config(lua_State *const l, LDServerConfigBuilder builder, struct c const char* key = lua_tostring(l, -1); int type = lua_type(l, -2); + DEBUG_PRINT("inspecting field %s (%s)\n", key, lua_typename(l, type)); + struct field_validator *field = find_field(key, cfg); if (field == NULL) { luaL_error(l, "unrecognized %s field: %s", cfg->name, key); @@ -602,7 +613,6 @@ void traverse_config(lua_State *const l, LDServerConfigBuilder builder, struct c luaL_error(l, "%s field %s must be a %s", cfg->name, key, lua_typename(l, field->type)); } if (field->parse != NULL) { - printf("parsing %s\n", key); field->parse(l, -2, builder, field->user_data); } else { luaL_error(l, "%s missing field parser for %s", cfg->name, key); @@ -635,7 +645,13 @@ makeConfig(lua_State *const l) // as we go along. traverse_config(l, builder, &top_level_config); - if (globalLogEnabledCallback != LUA_NOREF && globalLogWriteCallback != LUA_NOREF) { + bool logging_callbacks_set = + globalLogEnabledCallback != LUA_NOREF && + globalLogWriteCallback != LUA_NOREF; + + DEBUG_PRINT("logging callbacks set? %s\n", logging_callbacks_set ? "true" : "false"); + + if (logging_callbacks_set) { struct LDLogBackend backend; LDLogBackend_Init(&backend); From 8a9f3ccbc34fcf3fcc36c85faabef5fc4b20dff3 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Dec 2023 15:54:05 -0800 Subject: [PATCH 110/161] fewer unit tests --- test.lua | 190 +++++++++++++++++++++++++++---------------------------- 1 file changed, 95 insertions(+), 95 deletions(-) diff --git a/test.lua b/test.lua index a41709c..5b0b427 100644 --- a/test.lua +++ b/test.lua @@ -58,101 +58,101 @@ function TestAll:testSetAllConfigFields() } }) end - -function TestAll:testBoolVariation() - local e = false - u.assertEquals(makeTestClient():boolVariation(user, "test", e), e) -end - -function TestAll:testBoolVariationDetail() - local e = { - value = true, - reason = { - kind = "ERROR", - errorKind = "FLAG_NOT_FOUND", - inExperiment = false - } - } - u.assertEquals(makeTestClient():boolVariationDetail(user, "test", true), e) -end - -function TestAll:testIntVariation() - local e = 3 - u.assertEquals(makeTestClient():intVariation(user, "test", e), e) -end - -function TestAll:testIntVariationDetail() - local e = { - value = 5, - reason = { - kind = "ERROR", - errorKind = "FLAG_NOT_FOUND", - inExperiment = false - } - } - u.assertEquals(makeTestClient():intVariationDetail(user, "test", 5), e) -end - -function TestAll:testDoubleVariation() - local e = 12.5 - u.assertEquals(makeTestClient():doubleVariation(user, "test", e), e) -end - -function TestAll:testDoubleVariationDetail() - local e = { - value = 6.2, - reason = { - kind = "ERROR", - errorKind = "FLAG_NOT_FOUND", - inExperiment = false - } - } - u.assertEquals( makeTestClient():doubleVariationDetail(user, "test", 6.2), e) -end - -function TestAll:testStringVariation() - local e = "a" - u.assertEquals(makeTestClient():stringVariation(user, "test", e), e) -end - -function TestAll:testStringVariationDetail() - local e = { - value = "f", - reason = { - kind = "ERROR", - errorKind = "FLAG_NOT_FOUND", - inExperiment = false - } - } - u.assertEquals(makeTestClient():stringVariationDetail(user, "test", "f"), e) -end - -function TestAll:testJSONVariation() - local e = { ["a"] = "b" } - u.assertEquals(makeTestClient():jsonVariation(user, "test", e), e) -end - -function TestAll:testJSONVariationDetail() - local e = { - value = { a = "b" }, - reason = { - kind = "ERROR", - errorKind = "FLAG_NOT_FOUND", - inExperiment = false - } - } - u.assertEquals(makeTestClient():jsonVariationDetail(user, "test", { a = "b" }), e) -end - -function TestAll:testIdentify() - makeTestClient():identify(user) -end - -function TestAll:testVersion() - local version = l.version() - u.assertNotIsNil(version) - u.assertStrMatches(version, "(%d+)%.(%d+)%.(%d+)(.*)") -end +-- +--function TestAll:testBoolVariation() +-- local e = false +-- u.assertEquals(makeTestClient():boolVariation(user, "test", e), e) +--end +-- +--function TestAll:testBoolVariationDetail() +-- local e = { +-- value = true, +-- reason = { +-- kind = "ERROR", +-- errorKind = "FLAG_NOT_FOUND", +-- inExperiment = false +-- } +-- } +-- u.assertEquals(makeTestClient():boolVariationDetail(user, "test", true), e) +--end +-- +--function TestAll:testIntVariation() +-- local e = 3 +-- u.assertEquals(makeTestClient():intVariation(user, "test", e), e) +--end +-- +--function TestAll:testIntVariationDetail() +-- local e = { +-- value = 5, +-- reason = { +-- kind = "ERROR", +-- errorKind = "FLAG_NOT_FOUND", +-- inExperiment = false +-- } +-- } +-- u.assertEquals(makeTestClient():intVariationDetail(user, "test", 5), e) +--end +-- +--function TestAll:testDoubleVariation() +-- local e = 12.5 +-- u.assertEquals(makeTestClient():doubleVariation(user, "test", e), e) +--end +-- +--function TestAll:testDoubleVariationDetail() +-- local e = { +-- value = 6.2, +-- reason = { +-- kind = "ERROR", +-- errorKind = "FLAG_NOT_FOUND", +-- inExperiment = false +-- } +-- } +-- u.assertEquals( makeTestClient():doubleVariationDetail(user, "test", 6.2), e) +--end +-- +--function TestAll:testStringVariation() +-- local e = "a" +-- u.assertEquals(makeTestClient():stringVariation(user, "test", e), e) +--end +-- +--function TestAll:testStringVariationDetail() +-- local e = { +-- value = "f", +-- reason = { +-- kind = "ERROR", +-- errorKind = "FLAG_NOT_FOUND", +-- inExperiment = false +-- } +-- } +-- u.assertEquals(makeTestClient():stringVariationDetail(user, "test", "f"), e) +--end +-- +--function TestAll:testJSONVariation() +-- local e = { ["a"] = "b" } +-- u.assertEquals(makeTestClient():jsonVariation(user, "test", e), e) +--end +-- +--function TestAll:testJSONVariationDetail() +-- local e = { +-- value = { a = "b" }, +-- reason = { +-- kind = "ERROR", +-- errorKind = "FLAG_NOT_FOUND", +-- inExperiment = false +-- } +-- } +-- u.assertEquals(makeTestClient():jsonVariationDetail(user, "test", { a = "b" }), e) +--end +-- +--function TestAll:testIdentify() +-- makeTestClient():identify(user) +--end +-- +--function TestAll:testVersion() +-- local version = l.version() +-- u.assertNotIsNil(version) +-- u.assertStrMatches(version, "(%d+)%.(%d+)%.(%d+)(.*)") +--end local runner = u.LuaUnit.new() os.exit(runner:runSuite()) From e7a83d72b7b309878155817628af3286b8483797 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Dec 2023 15:55:35 -0800 Subject: [PATCH 111/161] less config --- test.lua | 50 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/test.lua b/test.lua index 5b0b427..3f6466a 100644 --- a/test.lua +++ b/test.lua @@ -31,31 +31,31 @@ end function TestAll:testSetAllConfigFields() local c = l.clientInit("sdk-test", { - offline = true, - appInfo = { - identifier = "MyApp", - version = "1.0.0" - }, - serviceEndpoints = { - streamingBaseURL = "foo", - pollingBaseURL = "bar", - eventsBaseURL = "baz" - }, - dataSystem = { - backgroundSync = { - streaming = { - initialReconnectDelayMilliseconds = 1000 - } - } - }, - events = { - capacity = 1000, - --contextKeysCapacity = 100, TODO: add once c binding available - enabled = true, - flushIntervalMilliseconds = 100, - allAttributesPrivate = true, - privateAttributes = {"/foo", "/bar"} - } + offline = true + --appInfo = { + -- identifier = "MyApp", + -- version = "1.0.0" + --}, + --serviceEndpoints = { + -- streamingBaseURL = "foo", + -- pollingBaseURL = "bar", + -- eventsBaseURL = "baz" + --}, + --dataSystem = { + -- backgroundSync = { + -- streaming = { + -- initialReconnectDelayMilliseconds = 1000 + -- } + -- } + --}, + --events = { + -- capacity = 1000, + -- --contextKeysCapacity = 100, TODO: add once c binding available + -- enabled = true, + -- flushIntervalMilliseconds = 100, + -- allAttributesPrivate = true, + -- privateAttributes = {"/foo", "/bar"} + --} }) end -- From 288201f22b27596637798eca08830e1e22ad3876 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Dec 2023 15:57:28 -0800 Subject: [PATCH 112/161] theory: table parsing casues segfault --- test.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test.lua b/test.lua index 3f6466a..e91df46 100644 --- a/test.lua +++ b/test.lua @@ -31,11 +31,11 @@ end function TestAll:testSetAllConfigFields() local c = l.clientInit("sdk-test", { - offline = true - --appInfo = { - -- identifier = "MyApp", - -- version = "1.0.0" - --}, + offline = true, + appInfo = { + identifier = "MyApp", + version = "1.0.0" + } --serviceEndpoints = { -- streamingBaseURL = "foo", -- pollingBaseURL = "bar", From b27effb57e0289375161f5c7dad7843796b2389d Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Dec 2023 15:59:52 -0800 Subject: [PATCH 113/161] try removing a lua pop inside the table traverse --- launchdarkly-server-sdk.c | 3 ++- test.lua | 40 +++++++++++++++++++-------------------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index cc3a882..724bed0 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -471,6 +471,7 @@ static void parse_table(lua_State *const l, int i, LDServerConfigBuilder builde // make it so. lua_pushvalue(l, i); traverse_config(l, builder, user_data); + lua_pop(l, 1); } @@ -619,7 +620,7 @@ void traverse_config(lua_State *const l, LDServerConfigBuilder builder, struct c } lua_pop(l, 2); } - lua_pop(l, 1); + // lua_pop(l, 1); } struct field_validator * find_field(const char *key, struct config* cfg) { diff --git a/test.lua b/test.lua index e91df46..5b0b427 100644 --- a/test.lua +++ b/test.lua @@ -35,27 +35,27 @@ function TestAll:testSetAllConfigFields() appInfo = { identifier = "MyApp", version = "1.0.0" + }, + serviceEndpoints = { + streamingBaseURL = "foo", + pollingBaseURL = "bar", + eventsBaseURL = "baz" + }, + dataSystem = { + backgroundSync = { + streaming = { + initialReconnectDelayMilliseconds = 1000 + } + } + }, + events = { + capacity = 1000, + --contextKeysCapacity = 100, TODO: add once c binding available + enabled = true, + flushIntervalMilliseconds = 100, + allAttributesPrivate = true, + privateAttributes = {"/foo", "/bar"} } - --serviceEndpoints = { - -- streamingBaseURL = "foo", - -- pollingBaseURL = "bar", - -- eventsBaseURL = "baz" - --}, - --dataSystem = { - -- backgroundSync = { - -- streaming = { - -- initialReconnectDelayMilliseconds = 1000 - -- } - -- } - --}, - --events = { - -- capacity = 1000, - -- --contextKeysCapacity = 100, TODO: add once c binding available - -- enabled = true, - -- flushIntervalMilliseconds = 100, - -- allAttributesPrivate = true, - -- privateAttributes = {"/foo", "/bar"} - --} }) end -- From 83493fa05e1f0c00568702e0b869552bc10845e4 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Dec 2023 16:02:16 -0800 Subject: [PATCH 114/161] test --- launchdarkly-server-sdk.c | 3 +-- test.lua | 40 +++++++++++++++++++-------------------- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 724bed0..cc3a882 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -471,7 +471,6 @@ static void parse_table(lua_State *const l, int i, LDServerConfigBuilder builde // make it so. lua_pushvalue(l, i); traverse_config(l, builder, user_data); - lua_pop(l, 1); } @@ -620,7 +619,7 @@ void traverse_config(lua_State *const l, LDServerConfigBuilder builder, struct c } lua_pop(l, 2); } - // lua_pop(l, 1); + lua_pop(l, 1); } struct field_validator * find_field(const char *key, struct config* cfg) { diff --git a/test.lua b/test.lua index 5b0b427..e91df46 100644 --- a/test.lua +++ b/test.lua @@ -35,27 +35,27 @@ function TestAll:testSetAllConfigFields() appInfo = { identifier = "MyApp", version = "1.0.0" - }, - serviceEndpoints = { - streamingBaseURL = "foo", - pollingBaseURL = "bar", - eventsBaseURL = "baz" - }, - dataSystem = { - backgroundSync = { - streaming = { - initialReconnectDelayMilliseconds = 1000 - } - } - }, - events = { - capacity = 1000, - --contextKeysCapacity = 100, TODO: add once c binding available - enabled = true, - flushIntervalMilliseconds = 100, - allAttributesPrivate = true, - privateAttributes = {"/foo", "/bar"} } + --serviceEndpoints = { + -- streamingBaseURL = "foo", + -- pollingBaseURL = "bar", + -- eventsBaseURL = "baz" + --}, + --dataSystem = { + -- backgroundSync = { + -- streaming = { + -- initialReconnectDelayMilliseconds = 1000 + -- } + -- } + --}, + --events = { + -- capacity = 1000, + -- --contextKeysCapacity = 100, TODO: add once c binding available + -- enabled = true, + -- flushIntervalMilliseconds = 100, + -- allAttributesPrivate = true, + -- privateAttributes = {"/foo", "/bar"} + --} }) end -- From 6aaecdbb06cdcb598ab0f4daee540fbdb830cef2 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Dec 2023 16:03:13 -0800 Subject: [PATCH 115/161] add serviceendpoints --- test.lua | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test.lua b/test.lua index e91df46..9ba56a5 100644 --- a/test.lua +++ b/test.lua @@ -35,12 +35,12 @@ function TestAll:testSetAllConfigFields() appInfo = { identifier = "MyApp", version = "1.0.0" - } - --serviceEndpoints = { - -- streamingBaseURL = "foo", - -- pollingBaseURL = "bar", - -- eventsBaseURL = "baz" - --}, + }, + serviceEndpoints = { + streamingBaseURL = "foo", + pollingBaseURL = "bar", + eventsBaseURL = "baz" + }, --dataSystem = { -- backgroundSync = { -- streaming = { From d7a2997a1a31c2bd920bfce9af8567de3e81c881 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Dec 2023 16:04:00 -0800 Subject: [PATCH 116/161] add datasystem --- launchdarkly-server-sdk.c | 6 +++--- test.lua | 15 ++++++++------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index cc3a882..1a0654c 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -546,9 +546,9 @@ struct field_validator backgroundsync_fields[] = { DEFINE_CONFIG(backgroundsync_config, "dataSystem.backgroundSync", backgroundsync_fields); struct field_validator datasystem_fields[] = { - {"enabled", LUA_TBOOLEAN, parse_bool, LDServerConfigBuilder_DataSystem_Enabled}, - {"backgroundSync", LUA_TTABLE, parse_table, &backgroundsync_config}, - {"lazyLoad", LUA_TTABLE, parse_table, &lazyload_config} + {"enabled", LUA_TBOOLEAN, parse_bool, LDServerConfigBuilder_DataSystem_Enabled} + //{"backgroundSync", LUA_TTABLE, parse_table, &backgroundsync_config}, + // {"lazyLoad", LUA_TTABLE, parse_table, &lazyload_config} }; DEFINE_CONFIG(datasystem_config, "dataSystem", datasystem_fields); diff --git a/test.lua b/test.lua index 9ba56a5..e73f33e 100644 --- a/test.lua +++ b/test.lua @@ -41,13 +41,14 @@ function TestAll:testSetAllConfigFields() pollingBaseURL = "bar", eventsBaseURL = "baz" }, - --dataSystem = { - -- backgroundSync = { - -- streaming = { - -- initialReconnectDelayMilliseconds = 1000 - -- } - -- } - --}, + dataSystem = { + enabled = true + --backgroundSync = { + -- streaming = { + -- initialReconnectDelayMilliseconds = 1000 + -- } + --} + }, --events = { -- capacity = 1000, -- --contextKeysCapacity = 100, TODO: add once c binding available From 9ce31bcf0a9d9657837a84f1e46805c75487caee Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Dec 2023 16:09:15 -0800 Subject: [PATCH 117/161] parse backgroundSync --- launchdarkly-server-sdk.c | 4 ++-- test.lua | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 1a0654c..8cd8887 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -546,8 +546,8 @@ struct field_validator backgroundsync_fields[] = { DEFINE_CONFIG(backgroundsync_config, "dataSystem.backgroundSync", backgroundsync_fields); struct field_validator datasystem_fields[] = { - {"enabled", LUA_TBOOLEAN, parse_bool, LDServerConfigBuilder_DataSystem_Enabled} - //{"backgroundSync", LUA_TTABLE, parse_table, &backgroundsync_config}, + {"enabled", LUA_TBOOLEAN, parse_bool, LDServerConfigBuilder_DataSystem_Enabled}, + {"backgroundSync", LUA_TTABLE, parse_table, &backgroundsync_config}, // {"lazyLoad", LUA_TTABLE, parse_table, &lazyload_config} }; diff --git a/test.lua b/test.lua index e73f33e..5a3de70 100644 --- a/test.lua +++ b/test.lua @@ -42,12 +42,12 @@ function TestAll:testSetAllConfigFields() eventsBaseURL = "baz" }, dataSystem = { - enabled = true - --backgroundSync = { + enabled = true, + backgroundSync = { -- streaming = { -- initialReconnectDelayMilliseconds = 1000 -- } - --} + } }, --events = { -- capacity = 1000, From c1e45d9369cc4080f7b6b01571b995b36cdf5885 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Dec 2023 16:12:03 -0800 Subject: [PATCH 118/161] add streaming --- test.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test.lua b/test.lua index 5a3de70..0655d47 100644 --- a/test.lua +++ b/test.lua @@ -44,9 +44,9 @@ function TestAll:testSetAllConfigFields() dataSystem = { enabled = true, backgroundSync = { - -- streaming = { + streaming = { -- initialReconnectDelayMilliseconds = 1000 - -- } + } } }, --events = { From a5dcff2964f1a201ecba9aeca0e87c1f6b81997b Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Dec 2023 16:13:16 -0800 Subject: [PATCH 119/161] add initialReconnect --- test.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test.lua b/test.lua index 0655d47..cdcf663 100644 --- a/test.lua +++ b/test.lua @@ -45,7 +45,7 @@ function TestAll:testSetAllConfigFields() enabled = true, backgroundSync = { streaming = { - -- initialReconnectDelayMilliseconds = 1000 + initialReconnectDelayMilliseconds = 1000 } } }, From af9c958469d4390fa83c51425695bb1938ae1047 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Dec 2023 16:16:27 -0800 Subject: [PATCH 120/161] grow stack --- launchdarkly-server-sdk.c | 1 + 1 file changed, 1 insertion(+) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 8cd8887..3406cc6 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -641,6 +641,7 @@ makeConfig(lua_State *const l) const char* sdk_key = luaL_checkstring(l, 1); LDServerConfigBuilder builder = LDServerConfigBuilder_New(sdk_key); + luaL_checkstack(l, 64, "not enough stack space for config struct"); // Recursively visit the heirarchical configs, modifying the builder // as we go along. traverse_config(l, builder, &top_level_config); From 8ba6b4c2d792335188c5f042ed528d641ef8ca44 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Dec 2023 16:18:18 -0800 Subject: [PATCH 121/161] theory: passes --- launchdarkly-server-sdk.c | 1 - test.lua | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 3406cc6..8cd8887 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -641,7 +641,6 @@ makeConfig(lua_State *const l) const char* sdk_key = luaL_checkstring(l, 1); LDServerConfigBuilder builder = LDServerConfigBuilder_New(sdk_key); - luaL_checkstack(l, 64, "not enough stack space for config struct"); // Recursively visit the heirarchical configs, modifying the builder // as we go along. traverse_config(l, builder, &top_level_config); diff --git a/test.lua b/test.lua index cdcf663..40980e0 100644 --- a/test.lua +++ b/test.lua @@ -45,7 +45,7 @@ function TestAll:testSetAllConfigFields() enabled = true, backgroundSync = { streaming = { - initialReconnectDelayMilliseconds = 1000 + --initialReconnectDelayMilliseconds = 1000 } } }, From d4870de653e21d450e8ed2d429ab28410743113b Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Dec 2023 16:21:09 -0800 Subject: [PATCH 122/161] theory: still broken --- test.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test.lua b/test.lua index 40980e0..ab9626a 100644 --- a/test.lua +++ b/test.lua @@ -44,8 +44,8 @@ function TestAll:testSetAllConfigFields() dataSystem = { enabled = true, backgroundSync = { - streaming = { - --initialReconnectDelayMilliseconds = 1000 + polling = { + intervalSeconds = 1000 } } }, From 98712288c3ec08c7bca0453a696ee273d1028e06 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Dec 2023 16:22:00 -0800 Subject: [PATCH 123/161] try changing function signature --- launchdarkly-server-sdk.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 8cd8887..959a0f8 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -452,7 +452,7 @@ static void parse_bool(lua_State *const l, int i, LDServerConfigBuilder builder, // The setter must have the signature (LDServerConfigBuilder, int). static void parse_number(lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data) { const int value = lua_tointeger(l, i); - void (*setter)(LDServerConfigBuilder, int) = user_data; + void (*setter)(LDServerConfigBuilder, unsigned int) = user_data; setter(builder, value); } From bff148bea3679d93468063f95c38c72deb7c07cf Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Dec 2023 16:26:45 -0800 Subject: [PATCH 124/161] theory: passes --- launchdarkly-server-sdk.c | 2 +- test.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 959a0f8..441e045 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -548,7 +548,7 @@ DEFINE_CONFIG(backgroundsync_config, "dataSystem.backgroundSync", backgroundsync struct field_validator datasystem_fields[] = { {"enabled", LUA_TBOOLEAN, parse_bool, LDServerConfigBuilder_DataSystem_Enabled}, {"backgroundSync", LUA_TTABLE, parse_table, &backgroundsync_config}, - // {"lazyLoad", LUA_TTABLE, parse_table, &lazyload_config} + {"lazyLoad", LUA_TTABLE, parse_table, &lazyload_config} }; DEFINE_CONFIG(datasystem_config, "dataSystem", datasystem_fields); diff --git a/test.lua b/test.lua index ab9626a..33802f4 100644 --- a/test.lua +++ b/test.lua @@ -45,7 +45,7 @@ function TestAll:testSetAllConfigFields() enabled = true, backgroundSync = { polling = { - intervalSeconds = 1000 + -- intervalSeconds = 1000 } } }, From 44fb819609703c9e7127a56c3de46b65a18435c6 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Dec 2023 16:28:07 -0800 Subject: [PATCH 125/161] theory: segfault --- test.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test.lua b/test.lua index 33802f4..ab9626a 100644 --- a/test.lua +++ b/test.lua @@ -45,7 +45,7 @@ function TestAll:testSetAllConfigFields() enabled = true, backgroundSync = { polling = { - -- intervalSeconds = 1000 + intervalSeconds = 1000 } } }, From 08451329f346ea4321f65383c258786e6184c557 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Dec 2023 16:34:07 -0800 Subject: [PATCH 126/161] maybeit's just numbers that are broken --- launchdarkly-server-sdk.c | 11 +++++++---- test.lua | 4 ++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 441e045..26e057a 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -435,15 +435,17 @@ struct field_validator { // Parses a string and then calls a setter function stored in user_data. // The setter must have the signature (LDServerConfigBuilder, const char*). static void parse_string(lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data) { - const char *const uri = lua_tostring(l, i); + const char *const value = lua_tostring(l, i); + DEBUG_PRINT("string = %s\n", value ? value : "NULL"); void (*setter)(LDServerConfigBuilder, const char*) = user_data; - setter(builder, uri); + setter(builder, value); } // Parses a bool and then calls a setter function stored in user_data. // The setter must have the signature (LDServerConfigBuilder, bool). static void parse_bool(lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data) { const bool value = lua_toboolean(l, i); + DEBUG_PRINT("bool = %s\n", value ? "true" : "false"); void (*setter)(LDServerConfigBuilder, bool) = user_data; setter(builder, value); } @@ -452,6 +454,7 @@ static void parse_bool(lua_State *const l, int i, LDServerConfigBuilder builder, // The setter must have the signature (LDServerConfigBuilder, int). static void parse_number(lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data) { const int value = lua_tointeger(l, i); + DEBUG_PRINT("number = %d\n", value); void (*setter)(LDServerConfigBuilder, unsigned int) = user_data; setter(builder, value); } @@ -525,13 +528,13 @@ struct field_validator lazyload_fields[] = { DEFINE_CONFIG(lazyload_config, "dataSystem.lazyLoad", lazyload_fields); struct field_validator streaming_fields[] = { - {"initialReconnectDelayMilliseconds", LUA_TNUMBER, parse_number, LDServerDataSourceStreamBuilder_InitialReconnectDelayMs}, + {"initialReconnectDelayMilliseconds", LUA_TBOOLEAN, parse_bool, LDServerConfigBuilder_DataSystem_Enabled} }; DEFINE_CONFIG(streaming_config, "dataSystem.backgroundSync.streaming", streaming_fields); struct field_validator polling_fields[] = { - {"intervalSeconds", LUA_TNUMBER, parse_number, LDServerDataSourcePollBuilder_IntervalS}, + {"intervalSeconds", LUA_TNUMBER, parse_number, LDServerDataSourcePollBuilder_IntervalS} }; DEFINE_CONFIG(polling_config, "dataSystem.backgroundSync.polling", polling_fields); diff --git a/test.lua b/test.lua index ab9626a..e5835de 100644 --- a/test.lua +++ b/test.lua @@ -44,8 +44,8 @@ function TestAll:testSetAllConfigFields() dataSystem = { enabled = true, backgroundSync = { - polling = { - intervalSeconds = 1000 + streaming = { + initialReconnectDelayMilliseconds = true } } }, From bea0aea6af099b780f88ba4a29aebcc3a5a5e9c0 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Dec 2023 16:52:46 -0800 Subject: [PATCH 127/161] go back to number --- launchdarkly-server-sdk.c | 8 +++++--- test.lua | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 26e057a..aeb934b 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -455,8 +455,10 @@ static void parse_bool(lua_State *const l, int i, LDServerConfigBuilder builder, static void parse_number(lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data) { const int value = lua_tointeger(l, i); DEBUG_PRINT("number = %d\n", value); - void (*setter)(LDServerConfigBuilder, unsigned int) = user_data; - setter(builder, value); + if (user_data) { + void (*setter)(LDServerConfigBuilder, unsigned int) = user_data; + setter(builder, value); + } } // Forward declaration of the config used in traverse_config, to keep parse_table @@ -528,7 +530,7 @@ struct field_validator lazyload_fields[] = { DEFINE_CONFIG(lazyload_config, "dataSystem.lazyLoad", lazyload_fields); struct field_validator streaming_fields[] = { - {"initialReconnectDelayMilliseconds", LUA_TBOOLEAN, parse_bool, LDServerConfigBuilder_DataSystem_Enabled} + {"initialReconnectDelayMilliseconds", LUA_TNUMBER, parse_number, NULL} }; DEFINE_CONFIG(streaming_config, "dataSystem.backgroundSync.streaming", streaming_fields); diff --git a/test.lua b/test.lua index e5835de..dbfd98c 100644 --- a/test.lua +++ b/test.lua @@ -45,7 +45,7 @@ function TestAll:testSetAllConfigFields() enabled = true, backgroundSync = { streaming = { - initialReconnectDelayMilliseconds = true + initialReconnectDelayMilliseconds = 10 } } }, From be508b9e23603693ae510628d6e7243c3eec336e Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Dec 2023 16:56:33 -0800 Subject: [PATCH 128/161] theory: crash --- launchdarkly-server-sdk.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index aeb934b..7cb1a2e 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -530,7 +530,7 @@ struct field_validator lazyload_fields[] = { DEFINE_CONFIG(lazyload_config, "dataSystem.lazyLoad", lazyload_fields); struct field_validator streaming_fields[] = { - {"initialReconnectDelayMilliseconds", LUA_TNUMBER, parse_number, NULL} + {"initialReconnectDelayMilliseconds", LUA_TNUMBER, parse_number, LDServerDataSourceStreamBuilder_InitialReconnectDelayMs} }; DEFINE_CONFIG(streaming_config, "dataSystem.backgroundSync.streaming", streaming_fields); From 5022a3f7fd2641e40518758a3c30ecf01c82962b Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Dec 2023 17:40:04 -0800 Subject: [PATCH 129/161] implement sub builders --- launchdarkly-server-sdk.c | 122 ++++++++++++++++++++++++++++---------- 1 file changed, 90 insertions(+), 32 deletions(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 7cb1a2e..b62993c 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -428,10 +428,17 @@ struct field_validator { // Function to parse the value on the top of the stack. If NULL, an error will be reported // at runtime if the field exists. void (*parse) (lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data); + // Alternative to parse if this field is for a sub-builder for a particular configuration item. + // In this case, the function should + void (*parse_sub_builder) (lua_State *const l, int i, void* sub_builder, void* user_data); // Store a pointer to arbitrary data for use in 'parse'. void *user_data; }; +#define FIELD(key, type, parse, user_data) {key, type, parse, NULL, user_data} + +#define FIELD_SUB_BUILDER(key, type, parse, user_data) {key, type, NULL, parse, user_data} + // Parses a string and then calls a setter function stored in user_data. // The setter must have the signature (LDServerConfigBuilder, const char*). static void parse_string(lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data) { @@ -461,6 +468,24 @@ static void parse_number(lua_State *const l, int i, LDServerConfigBuilder builde } } +static void parse_number_stream(lua_State *const l, int i, LDServerDataSourceStreamBuilder builder, void* user_data) { + const int value = lua_tointeger(l, i); + DEBUG_PRINT("number = %d\n", value); + if (user_data) { + void (*setter)(LDServerDataSourceStreamBuilder, unsigned int) = user_data; + setter(builder, value); + } +} + +static void parse_number_poll(lua_State *const l, int i, LDServerDataSourcePollBuilder builder, void* user_data) { + const int value = lua_tointeger(l, i); + DEBUG_PRINT("number = %d\n", value); + if (user_data) { + void (*setter)(LDServerDataSourcePollBuilder, unsigned int) = user_data; + setter(builder, value); + } +} + // Forward declaration of the config used in traverse_config, to keep parse_table // in the same place as the others. struct config; @@ -512,84 +537,105 @@ struct config { const char *name; struct field_validator* fields; int n; + + void *builder; + void* (*new_builder)(void); + void (*set_builder)(LDServerConfigBuilder, void*); + }; #define ARR_SIZE(x) (sizeof(x) / sizeof(x[0])) // Use this macro to define new config tables. Config tables can be nested arbitrarily. #define DEFINE_CONFIG(name, path, fields) \ - struct config name = {path, fields, ARR_SIZE(fields)} + struct config name = {path, fields, ARR_SIZE(fields), NULL, NULL, NULL} + +#define DEFINE_SUB_CONFIG(name, path, fields, new_builder, set_builder) \ + struct config name = {path, fields, ARR_SIZE(fields), NULL, new_builder, set_builder} +bool config_invoke_parser(struct config *cfg, struct field_validator *field, LDServerConfigBuilder builder, lua_State *const l) { + if (field->parse != NULL) { + DEBUG_PRINT("invoking normal parser for %s\n", field->key); + field->parse(l, -2, builder, field->user_data); + return true; + } + if (field->parse_sub_builder != NULL) { + DEBUG_PRINT("invoking sub-builder parser for %s\n", field->key); + field->parse_sub_builder(l, -2, cfg->builder, field->user_data); + return true; + } + return false; +} struct field_validator lazyload_fields[] = { - {"source", LUA_TUSERDATA, parse_lazyload_source, NULL /* not needed */}, - {"cacheRefreshMilliseconds", LUA_TNUMBER, parse_number, LDServerLazyLoadBuilder_CacheRefreshMs}, - {"cacheEvictionPolicy", LUA_TNUMBER, parse_number, LDServerLazyLoadBuilder_CachePolicy} + FIELD("source", LUA_TUSERDATA, parse_lazyload_source, NULL), + FIELD("cacheRefreshMilliseconds", LUA_TNUMBER, parse_number, LDServerLazyLoadBuilder_CacheRefreshMs), + FIELD("cacheEvictionPolicy", LUA_TNUMBER, parse_number, LDServerLazyLoadBuilder_CachePolicy) }; DEFINE_CONFIG(lazyload_config, "dataSystem.lazyLoad", lazyload_fields); struct field_validator streaming_fields[] = { - {"initialReconnectDelayMilliseconds", LUA_TNUMBER, parse_number, LDServerDataSourceStreamBuilder_InitialReconnectDelayMs} + FIELD_SUB_BUILDER("initialReconnectDelayMilliseconds", LUA_TNUMBER, parse_number_stream, LDServerDataSourceStreamBuilder_InitialReconnectDelayMs) }; -DEFINE_CONFIG(streaming_config, "dataSystem.backgroundSync.streaming", streaming_fields); +DEFINE_SUB_CONFIG(streaming_config, "dataSystem.backgroundSync.streaming", streaming_fields, LDServerDataSourceStreamBuilder_New, LDServerConfigBuilder_DataSystem_BackgroundSync_Streaming); struct field_validator polling_fields[] = { - {"intervalSeconds", LUA_TNUMBER, parse_number, LDServerDataSourcePollBuilder_IntervalS} + FIELD_SUB_BUILDER("intervalSeconds", LUA_TNUMBER, parse_number_poll, LDServerDataSourcePollBuilder_IntervalS) }; -DEFINE_CONFIG(polling_config, "dataSystem.backgroundSync.polling", polling_fields); +DEFINE_SUB_CONFIG(polling_config, "dataSystem.backgroundSync.polling", polling_fields, LDServerDataSourcePollBuilder_New, LDServerConfigBuilder_DataSystem_BackgroundSync_Polling); struct field_validator backgroundsync_fields[] = { /* Mutually exclusive */ - {"streaming", LUA_TTABLE, parse_table, &streaming_config}, - {"polling", LUA_TTABLE, parse_table, &polling_config} + FIELD("streaming", LUA_TTABLE, parse_table, &streaming_config), + FIELD("polling", LUA_TTABLE, parse_table, &polling_config) }; DEFINE_CONFIG(backgroundsync_config, "dataSystem.backgroundSync", backgroundsync_fields); struct field_validator datasystem_fields[] = { - {"enabled", LUA_TBOOLEAN, parse_bool, LDServerConfigBuilder_DataSystem_Enabled}, - {"backgroundSync", LUA_TTABLE, parse_table, &backgroundsync_config}, - {"lazyLoad", LUA_TTABLE, parse_table, &lazyload_config} + FIELD("enabled", LUA_TBOOLEAN, parse_bool, LDServerConfigBuilder_DataSystem_Enabled), + FIELD("backgroundSync", LUA_TTABLE, parse_table, &backgroundsync_config), + FIELD("lazyLoad", LUA_TTABLE, parse_table, &lazyload_config) }; DEFINE_CONFIG(datasystem_config, "dataSystem", datasystem_fields); struct field_validator event_fields[] = { - {"enabled", LUA_TBOOLEAN, parse_bool, LDServerConfigBuilder_Events_Enabled}, - {"contextKeysCapacity", LUA_TNUMBER, NULL, NULL}, - {"capacity", LUA_TNUMBER, parse_number, LDServerConfigBuilder_Events_Capacity}, - {"flushIntervalMilliseconds", LUA_TNUMBER, parse_number, LDServerConfigBuilder_Events_FlushIntervalMs}, - {"allAttributesPrivate", LUA_TBOOLEAN, parse_bool, LDServerConfigBuilder_Events_AllAttributesPrivate}, - {"privateAttributes", LUA_TTABLE, parse_string_array, LDServerConfigBuilder_Events_PrivateAttribute} + FIELD("enabled", LUA_TBOOLEAN, parse_bool, LDServerConfigBuilder_Events_Enabled), + FIELD("contextKeysCapacity", LUA_TNUMBER, NULL, NULL), + FIELD("capacity", LUA_TNUMBER, parse_number, LDServerConfigBuilder_Events_Capacity), + FIELD("flushIntervalMilliseconds", LUA_TNUMBER, parse_number, LDServerConfigBuilder_Events_FlushIntervalMs), + FIELD("allAttributesPrivate", LUA_TBOOLEAN, parse_bool, LDServerConfigBuilder_Events_AllAttributesPrivate), + FIELD("privateAttributes", LUA_TTABLE, parse_string_array, LDServerConfigBuilder_Events_PrivateAttribute) }; DEFINE_CONFIG(event_config, "events", event_fields); struct field_validator endpoint_fields[] = { - {"pollingBaseURL", LUA_TSTRING, parse_string, LDServerConfigBuilder_ServiceEndpoints_PollingBaseURL}, - {"streamingBaseURL", LUA_TSTRING, parse_string, LDServerConfigBuilder_ServiceEndpoints_StreamingBaseURL}, - {"eventsBaseURL", LUA_TSTRING, parse_string, LDServerConfigBuilder_ServiceEndpoints_EventsBaseURL} + FIELD("pollingBaseURL", LUA_TSTRING, parse_string, LDServerConfigBuilder_ServiceEndpoints_PollingBaseURL), + FIELD("streamingBaseURL", LUA_TSTRING, parse_string, LDServerConfigBuilder_ServiceEndpoints_StreamingBaseURL), + FIELD("eventsBaseURL", LUA_TSTRING, parse_string, LDServerConfigBuilder_ServiceEndpoints_EventsBaseURL) }; DEFINE_CONFIG(endpoint_config, "serviceEndpoints", endpoint_fields); struct field_validator appinfo_fields[] = { - {"identifier", LUA_TSTRING, parse_string, LDServerConfigBuilder_AppInfo_Identifier}, - {"version", LUA_TSTRING, parse_string, LDServerConfigBuilder_AppInfo_Version} + FIELD("identifier", LUA_TSTRING, parse_string, LDServerConfigBuilder_AppInfo_Identifier), + FIELD("version", LUA_TSTRING, parse_string, LDServerConfigBuilder_AppInfo_Version) }; DEFINE_CONFIG(appinfo_config, "appInfo", appinfo_fields); struct field_validator top_level_fields[] = { - {"appInfo", LUA_TTABLE, parse_table, &appinfo_config}, - {"serviceEndpoints", LUA_TTABLE, parse_table, &endpoint_config}, - {"offline", LUA_TBOOLEAN, parse_bool, LDServerConfigBuilder_Offline}, - {"dataSystem", LUA_TTABLE, parse_table, &datasystem_config }, - {"events", LUA_TTABLE, parse_table, &event_config } + FIELD("appInfo", LUA_TTABLE, parse_table, &appinfo_config), + FIELD("serviceEndpoints", LUA_TTABLE, parse_table, &endpoint_config), + FIELD("offline", LUA_TBOOLEAN, parse_bool, LDServerConfigBuilder_Offline), + FIELD("dataSystem", LUA_TTABLE, parse_table, &datasystem_config), + FIELD("events", LUA_TTABLE, parse_table, &event_config) }; DEFINE_CONFIG(top_level_config, "config", top_level_fields); @@ -602,6 +648,12 @@ void traverse_config(lua_State *const l, LDServerConfigBuilder builder, struct c if (lua_type(l, -1) != LUA_TTABLE) { luaL_error(l, "%s must be a table", cfg->name); } + + if (cfg->new_builder != NULL) { + cfg->builder = cfg->new_builder(); + DEBUG_PRINT("created sub-builder (%p)\n", cfg->builder); + } + lua_pushnil(l); while (lua_next(l, -2) != 0) { lua_pushvalue(l, -2); @@ -617,13 +669,19 @@ void traverse_config(lua_State *const l, LDServerConfigBuilder builder, struct c if (field->type != type) { luaL_error(l, "%s field %s must be a %s", cfg->name, key, lua_typename(l, field->type)); } - if (field->parse != NULL) { - field->parse(l, -2, builder, field->user_data); - } else { + + if (!config_invoke_parser(cfg, field, builder, l)) { luaL_error(l, "%s missing field parser for %s", cfg->name, key); } + lua_pop(l, 2); } + + if (cfg->builder != NULL) { + DEBUG_PRINT("invoking sub-builder setter (%p) on sub-builder (%p)\n", cfg->set_builder, cfg->builder); + cfg->set_builder(builder, cfg->builder); + } + lua_pop(l, 1); } From 2641b5c09263cbd02d37a4dc4a030272f9263ca3 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Dec 2023 18:30:29 -0800 Subject: [PATCH 130/161] cleanup --- launchdarkly-server-sdk.c | 165 +++++++++++++++++++------------------- 1 file changed, 82 insertions(+), 83 deletions(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index b62993c..3f0d631 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -420,69 +420,64 @@ LuaLDUserFree(lua_State *const l) } // field_validator is used to validate a single field in a config table. +// The field delegates to a parse function, which handles extracting the actual +// type. struct field_validator { - // Name of the field. + // Name of the field used in Lua. const char* key; + // Expected Lua type of the field. int type; - // Function to parse the value on the top of the stack. If NULL, an error will be reported - // at runtime if the field exists. - void (*parse) (lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data); - // Alternative to parse if this field is for a sub-builder for a particular configuration item. - // In this case, the function should - void (*parse_sub_builder) (lua_State *const l, int i, void* sub_builder, void* user_data); - // Store a pointer to arbitrary data for use in 'parse'. - void *user_data; -}; -#define FIELD(key, type, parse, user_data) {key, type, parse, NULL, user_data} + // Function that parses the value at stack index i. + // + // The function must agree on the type of the argument 'setter', which is + // the value of this struct's 'setter' field. + // + // For example, parse might handle parsing bools. So the actual + // signature for 'setter' might be: + // void (*setter)(LDServerConfigBuilder, bool) + // + // Which would allow the implementation of parse to call setter(builder, value). + void (*parse) (lua_State *const l, int i, void* builder, void* setter); + + // Stores a function that is capable of setting a value on an arbitrary builder. + // The type of the value being stored is erased here so that field_validator + // can handle all necessary types. + void *setter; +}; -#define FIELD_SUB_BUILDER(key, type, parse, user_data) {key, type, NULL, parse, user_data} +#define FIELD(key, type, parse, user_data) {key, type, parse, user_data} -// Parses a string and then calls a setter function stored in user_data. -// The setter must have the signature (LDServerConfigBuilder, const char*). -static void parse_string(lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data) { +// Parses a string. +// The setter must have the signature (void*, const char*). +static void parse_string(lua_State *const l, int i, void* builder, void* setter) { const char *const value = lua_tostring(l, i); DEBUG_PRINT("string = %s\n", value ? value : "NULL"); - void (*setter)(LDServerConfigBuilder, const char*) = user_data; - setter(builder, value); + void (*string_setter)(void*, const char*) = setter; + string_setter(builder, value); } -// Parses a bool and then calls a setter function stored in user_data. -// The setter must have the signature (LDServerConfigBuilder, bool). -static void parse_bool(lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data) { +// Parses a bool. +// The setter must have the signature (void*, bool). +static void parse_bool(lua_State *const l, int i, void* builder, void* setter) { const bool value = lua_toboolean(l, i); DEBUG_PRINT("bool = %s\n", value ? "true" : "false"); - void (*setter)(LDServerConfigBuilder, bool) = user_data; - setter(builder, value); + void (*bool_setter)(void*, bool) = setter; + bool_setter(builder, value); } -// Parses a number and then calls a setter function stored in user_data. -// The setter must have the signature (LDServerConfigBuilder, int). -static void parse_number(lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data) { +// Parses a number. +// The setter must have the signature (void*, unsigned int). +static void parse_unsigned(lua_State *const l, int i, void* builder, void* setter) { const int value = lua_tointeger(l, i); DEBUG_PRINT("number = %d\n", value); - if (user_data) { - void (*setter)(LDServerConfigBuilder, unsigned int) = user_data; - setter(builder, value); + if (value < 0) { + luaL_error(l, "got %d, expected positive int", value); } -} - -static void parse_number_stream(lua_State *const l, int i, LDServerDataSourceStreamBuilder builder, void* user_data) { - const int value = lua_tointeger(l, i); - DEBUG_PRINT("number = %d\n", value); - if (user_data) { - void (*setter)(LDServerDataSourceStreamBuilder, unsigned int) = user_data; - setter(builder, value); - } -} - -static void parse_number_poll(lua_State *const l, int i, LDServerDataSourcePollBuilder builder, void* user_data) { - const int value = lua_tointeger(l, i); - DEBUG_PRINT("number = %d\n", value); - if (user_data) { - void (*setter)(LDServerDataSourcePollBuilder, unsigned int) = user_data; - setter(builder, value); + if (setter) { + void (*unsigned_int_setter)(void*, unsigned int) = setter; + unsigned_int_setter(builder, value); } } @@ -495,19 +490,21 @@ struct config; // on top of the stack. void traverse_config(lua_State *const l, LDServerConfigBuilder builder, struct config *cfg); -// Parses a table using traverse_config. -static void parse_table(lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data) { +// Parses a table using traverse_config. Can only be invoked on top level LDServerConfigBuilder +// configurations, not sub-builders. +static void parse_table(lua_State *const l, int i, void* builder, void* user_data) { // since traverse_config expects the table to be on top of the stack, // make it so. lua_pushvalue(l, i); - traverse_config(l, builder, user_data); + traverse_config(l, (LDServerConfigBuilder) builder, user_data); } -// Parses an array of strings. Items that aren't strings are silently ignored. -static void parse_string_array(lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data) { - void (*setter)(LDServerConfigBuilder, const char*) = user_data; +// Parses an array of strings. Items that aren't strings will trigger an error. +// The setter must have the signature (void*, const char*). +static void parse_string_array(lua_State *const l, int i, void* builder, void* setter) { + void (*string_setter)(void*, const char*) = setter; int n = lua_tablelen(l, i); DEBUG_PRINT("parsing string array of length %d\n", n); @@ -516,22 +513,27 @@ static void parse_string_array(lua_State *const l, int i, LDServerConfigBuilder if (lua_isstring(l, -1)) { const char* elem = lua_tostring(l, -1); DEBUG_PRINT("array[%d] = %s\n", j, elem); - setter(builder, elem); + string_setter(builder, elem); + } else { + luaL_error(l, "array[%d] is not a string", j); } lua_pop(l, 1); } } // Special purpose parser for grabbing a store interface from a userdata. -static void parse_lazyload_source(lua_State *const l, int i, LDServerConfigBuilder builder, void* user_data) { -// TODO: replace checkudata +static void parse_lazyload_source(lua_State *const l, int i, void* builder, void* setter) { + // TODO: check that this implements the correct userdata. LDServerLazyLoadSourcePtr *source = lua_touserdata(l, i); - LDServerLazyLoadBuilder lazy_load_builder = LDServerLazyLoadBuilder_New(); - LDServerLazyLoadBuilder_SourcePtr(lazy_load_builder, *source); - LDServerConfigBuilder_DataSystem_LazyLoad(builder, lazy_load_builder); + void (*source_setter)(void*, void*) = setter; + source_setter(builder, source); } + +typedef void* (*new_builder_fn)(void); +typedef void (*consume_builder_fn)(LDServerConfigBuilder, void*); + // Stores a list of fields and the field count. The name is used for error reporting. struct config { const char *name; @@ -539,8 +541,8 @@ struct config { int n; void *builder; - void* (*new_builder)(void); - void (*set_builder)(LDServerConfigBuilder, void*); + new_builder_fn new_builder; + consume_builder_fn consume_builder; }; @@ -550,39 +552,35 @@ struct config { #define DEFINE_CONFIG(name, path, fields) \ struct config name = {path, fields, ARR_SIZE(fields), NULL, NULL, NULL} -#define DEFINE_SUB_CONFIG(name, path, fields, new_builder, set_builder) \ - struct config name = {path, fields, ARR_SIZE(fields), NULL, new_builder, set_builder} +#define DEFINE_SUB_CONFIG(name, path, fields, new_builder, consume_builder) \ + struct config name = {path, fields, ARR_SIZE(fields), NULL, (new_builder_fn) new_builder, (consume_builder_fn) consume_builder} -bool config_invoke_parser(struct config *cfg, struct field_validator *field, LDServerConfigBuilder builder, lua_State *const l) { - if (field->parse != NULL) { - DEBUG_PRINT("invoking normal parser for %s\n", field->key); - field->parse(l, -2, builder, field->user_data); - return true; - } - if (field->parse_sub_builder != NULL) { +void config_invoke_parser(struct config *cfg, struct field_validator *field, LDServerConfigBuilder builder, lua_State *const l) { + if (cfg->builder) { DEBUG_PRINT("invoking sub-builder parser for %s\n", field->key); - field->parse_sub_builder(l, -2, cfg->builder, field->user_data); - return true; + field->parse(l, -2, cfg->builder, field->setter); + } else { + DEBUG_PRINT("invoking normal parser for %s\n", field->key); + field->parse(l, -2, builder, field->setter); } - return false; } struct field_validator lazyload_fields[] = { - FIELD("source", LUA_TUSERDATA, parse_lazyload_source, NULL), - FIELD("cacheRefreshMilliseconds", LUA_TNUMBER, parse_number, LDServerLazyLoadBuilder_CacheRefreshMs), - FIELD("cacheEvictionPolicy", LUA_TNUMBER, parse_number, LDServerLazyLoadBuilder_CachePolicy) + FIELD("source", LUA_TUSERDATA, parse_lazyload_source, LDServerLazyLoadBuilder_SourcePtr), + FIELD("cacheRefreshMilliseconds", LUA_TNUMBER, parse_unsigned, LDServerLazyLoadBuilder_CacheRefreshMs), + FIELD("cacheEvictionPolicy", LUA_TNUMBER, parse_unsigned, LDServerLazyLoadBuilder_CachePolicy) }; -DEFINE_CONFIG(lazyload_config, "dataSystem.lazyLoad", lazyload_fields); +DEFINE_SUB_CONFIG(lazyload_config, "dataSystem.lazyLoad", lazyload_fields, LDServerLazyLoadBuilder_New, LDServerConfigBuilder_DataSystem_LazyLoad); struct field_validator streaming_fields[] = { - FIELD_SUB_BUILDER("initialReconnectDelayMilliseconds", LUA_TNUMBER, parse_number_stream, LDServerDataSourceStreamBuilder_InitialReconnectDelayMs) + FIELD("initialReconnectDelayMilliseconds", LUA_TNUMBER, parse_unsigned, LDServerDataSourceStreamBuilder_InitialReconnectDelayMs) }; DEFINE_SUB_CONFIG(streaming_config, "dataSystem.backgroundSync.streaming", streaming_fields, LDServerDataSourceStreamBuilder_New, LDServerConfigBuilder_DataSystem_BackgroundSync_Streaming); struct field_validator polling_fields[] = { - FIELD_SUB_BUILDER("intervalSeconds", LUA_TNUMBER, parse_number_poll, LDServerDataSourcePollBuilder_IntervalS) + FIELD("intervalSeconds", LUA_TNUMBER, parse_unsigned, LDServerDataSourcePollBuilder_IntervalS) }; DEFINE_SUB_CONFIG(polling_config, "dataSystem.backgroundSync.polling", polling_fields, LDServerDataSourcePollBuilder_New, LDServerConfigBuilder_DataSystem_BackgroundSync_Polling); @@ -607,8 +605,8 @@ DEFINE_CONFIG(datasystem_config, "dataSystem", datasystem_fields); struct field_validator event_fields[] = { FIELD("enabled", LUA_TBOOLEAN, parse_bool, LDServerConfigBuilder_Events_Enabled), FIELD("contextKeysCapacity", LUA_TNUMBER, NULL, NULL), - FIELD("capacity", LUA_TNUMBER, parse_number, LDServerConfigBuilder_Events_Capacity), - FIELD("flushIntervalMilliseconds", LUA_TNUMBER, parse_number, LDServerConfigBuilder_Events_FlushIntervalMs), + FIELD("capacity", LUA_TNUMBER, parse_unsigned, LDServerConfigBuilder_Events_Capacity), + FIELD("flushIntervalMilliseconds", LUA_TNUMBER, parse_unsigned, LDServerConfigBuilder_Events_FlushIntervalMs), FIELD("allAttributesPrivate", LUA_TBOOLEAN, parse_bool, LDServerConfigBuilder_Events_AllAttributesPrivate), FIELD("privateAttributes", LUA_TTABLE, parse_string_array, LDServerConfigBuilder_Events_PrivateAttribute) }; @@ -669,17 +667,18 @@ void traverse_config(lua_State *const l, LDServerConfigBuilder builder, struct c if (field->type != type) { luaL_error(l, "%s field %s must be a %s", cfg->name, key, lua_typename(l, field->type)); } - - if (!config_invoke_parser(cfg, field, builder, l)) { + if (field->parse == NULL) { luaL_error(l, "%s missing field parser for %s", cfg->name, key); + } else { + config_invoke_parser(cfg, field, builder, l); } lua_pop(l, 2); } if (cfg->builder != NULL) { - DEBUG_PRINT("invoking sub-builder setter (%p) on sub-builder (%p)\n", cfg->set_builder, cfg->builder); - cfg->set_builder(builder, cfg->builder); + DEBUG_PRINT("invoking sub-builder consumer (%p) on sub-builder (%p)\n", cfg->consume_builder, cfg->builder); + cfg->consume_builder(builder, cfg->builder); } lua_pop(l, 1); From 7392b54c49f947d06c8a5ea105a1724a1f3cd073 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Dec 2023 18:55:10 -0800 Subject: [PATCH 131/161] fix segfault --- launchdarkly-server-sdk.c | 106 ++++++++++++++++++++++++++------------ redis-test.lua | 1 + 2 files changed, 73 insertions(+), 34 deletions(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 3f0d631..fc56502 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -491,7 +491,7 @@ struct config; void traverse_config(lua_State *const l, LDServerConfigBuilder builder, struct config *cfg); // Parses a table using traverse_config. Can only be invoked on top level LDServerConfigBuilder -// configurations, not sub-builders. +// configurations, not child builders. static void parse_table(lua_State *const l, int i, void* builder, void* user_data) { // since traverse_config expects the table to be on top of the stack, // make it so. @@ -499,8 +499,6 @@ static void parse_table(lua_State *const l, int i, void* builder, void* user_dat traverse_config(l, (LDServerConfigBuilder) builder, user_data); } - - // Parses an array of strings. Items that aren't strings will trigger an error. // The setter must have the signature (void*, const char*). static void parse_string_array(lua_State *const l, int i, void* builder, void* setter) { @@ -526,41 +524,65 @@ static void parse_lazyload_source(lua_State *const l, int i, void* builder, void // TODO: check that this implements the correct userdata. LDServerLazyLoadSourcePtr *source = lua_touserdata(l, i); - void (*source_setter)(void*, void*) = setter; - source_setter(builder, source); -} + DEBUG_PRINT("source = %p\n", *source); + void (*source_setter)(void*, void*) = setter; -typedef void* (*new_builder_fn)(void); -typedef void (*consume_builder_fn)(LDServerConfigBuilder, void*); + // Dereferencing source because lua_touserdata returns a pointer (to our pointer). + source_setter(builder, *source); +} -// Stores a list of fields and the field count. The name is used for error reporting. +// Function that returns a new child builder. This is used to allocate builders +// which are necessary for building a child config. This is needed when the C++ SDK's +// API requires a builder to be allocated and passed in to the top-level builder, e.g. +// the streaming or polling config builders. +typedef void* (*new_child_builder_fn)(void); + +// Function that consumes the builder created by a new_child_builder_fn. This passes +// ownership of the builder back to the top-level configuration builder. +typedef void (*consume_child_builder_fn)(LDServerConfigBuilder, void*); + +// Represents a logical chunk of config with a list of fields. +// Individual fields might themselves be configs. +// +// If a child config requires its own builder, then new_builder and consume_builder must be set. +// In this case, before any fields are parsed, new_builder will be invoked +// and stored in child_builder. After all fields are parsed, consume_builder will be invoked +// to transfer ownership of the child to the parent top-level config. struct config { + // Name of the config, used for errors / logging. const char *name; + + // List of fields and length. struct field_validator* fields; int n; - void *builder; - new_builder_fn new_builder; - consume_builder_fn consume_builder; + // Null at compile-time; stores the result of new_builder at runtime (if set.) + void *child_builder; + // Assign both if this config needs a child builder. + new_child_builder_fn new_child_builder; + consume_child_builder_fn consume_child_builder; }; #define ARR_SIZE(x) (sizeof(x) / sizeof(x[0])) -// Use this macro to define new config tables. Config tables can be nested arbitrarily. +// Use this macro to define new config tables. #define DEFINE_CONFIG(name, path, fields) \ struct config name = {path, fields, ARR_SIZE(fields), NULL, NULL, NULL} +// Use this macro to define a config table which requires a child builder. #define DEFINE_SUB_CONFIG(name, path, fields, new_builder, consume_builder) \ - struct config name = {path, fields, ARR_SIZE(fields), NULL, (new_builder_fn) new_builder, (consume_builder_fn) consume_builder} - -void config_invoke_parser(struct config *cfg, struct field_validator *field, LDServerConfigBuilder builder, lua_State *const l) { - if (cfg->builder) { - DEBUG_PRINT("invoking sub-builder parser for %s\n", field->key); - field->parse(l, -2, cfg->builder, field->setter); + struct config name = {path, fields, ARR_SIZE(fields), NULL, (new_child_builder_fn) new_builder, (consume_child_builder_fn) consume_builder} + +// Invokes a field's parse method, varying the builder argument depending on if this +// is a top-level or child config. +void config_invoke_parse(struct config *cfg, struct field_validator *field, LDServerConfigBuilder builder, lua_State *const l) { + if (cfg->child_builder) { + DEBUG_PRINT("invoking parser for %s with child builder (%p)\n", field->key, cfg->child_builder); + field->parse(l, -2, cfg->child_builder, field->setter); } else { - DEBUG_PRINT("invoking normal parser for %s\n", field->key); + DEBUG_PRINT("invoking parser for %s with top-level builder\n", field->key); field->parse(l, -2, builder, field->setter); } } @@ -571,19 +593,34 @@ struct field_validator lazyload_fields[] = { FIELD("cacheEvictionPolicy", LUA_TNUMBER, parse_unsigned, LDServerLazyLoadBuilder_CachePolicy) }; -DEFINE_SUB_CONFIG(lazyload_config, "dataSystem.lazyLoad", lazyload_fields, LDServerLazyLoadBuilder_New, LDServerConfigBuilder_DataSystem_LazyLoad); +DEFINE_SUB_CONFIG(lazyload_config, + "dataSystem.lazyLoad", + lazyload_fields, + LDServerLazyLoadBuilder_New, + LDServerConfigBuilder_DataSystem_LazyLoad +); struct field_validator streaming_fields[] = { FIELD("initialReconnectDelayMilliseconds", LUA_TNUMBER, parse_unsigned, LDServerDataSourceStreamBuilder_InitialReconnectDelayMs) }; -DEFINE_SUB_CONFIG(streaming_config, "dataSystem.backgroundSync.streaming", streaming_fields, LDServerDataSourceStreamBuilder_New, LDServerConfigBuilder_DataSystem_BackgroundSync_Streaming); +DEFINE_SUB_CONFIG(streaming_config, + "dataSystem.backgroundSync.streaming", + streaming_fields, + LDServerDataSourceStreamBuilder_New, + LDServerConfigBuilder_DataSystem_BackgroundSync_Streaming +); struct field_validator polling_fields[] = { FIELD("intervalSeconds", LUA_TNUMBER, parse_unsigned, LDServerDataSourcePollBuilder_IntervalS) }; -DEFINE_SUB_CONFIG(polling_config, "dataSystem.backgroundSync.polling", polling_fields, LDServerDataSourcePollBuilder_New, LDServerConfigBuilder_DataSystem_BackgroundSync_Polling); +DEFINE_SUB_CONFIG(polling_config, + "dataSystem.backgroundSync.polling", + polling_fields, + LDServerDataSourcePollBuilder_New, + LDServerConfigBuilder_DataSystem_BackgroundSync_Polling +); struct field_validator backgroundsync_fields[] = { /* Mutually exclusive */ @@ -647,9 +684,9 @@ void traverse_config(lua_State *const l, LDServerConfigBuilder builder, struct c luaL_error(l, "%s must be a table", cfg->name); } - if (cfg->new_builder != NULL) { - cfg->builder = cfg->new_builder(); - DEBUG_PRINT("created sub-builder (%p)\n", cfg->builder); + if (cfg->new_child_builder != NULL) { + cfg->child_builder = cfg->new_child_builder(); + DEBUG_PRINT("created child builder (%p) for %s\n", cfg->child_builder, cfg->name); } lua_pushnil(l); @@ -670,15 +707,15 @@ void traverse_config(lua_State *const l, LDServerConfigBuilder builder, struct c if (field->parse == NULL) { luaL_error(l, "%s missing field parser for %s", cfg->name, key); } else { - config_invoke_parser(cfg, field, builder, l); + config_invoke_parse(cfg, field, builder, l); } lua_pop(l, 2); } - if (cfg->builder != NULL) { - DEBUG_PRINT("invoking sub-builder consumer (%p) on sub-builder (%p)\n", cfg->consume_builder, cfg->builder); - cfg->consume_builder(builder, cfg->builder); + if (cfg->child_builder != NULL) { + DEBUG_PRINT("invoking child builder consumer (%p) on child builder (%p)\n", cfg->consume_child_builder, cfg->child_builder); + cfg->consume_child_builder(builder, cfg->child_builder); } lua_pop(l, 1); @@ -697,8 +734,8 @@ static LDServerConfig makeConfig(lua_State *const l) { // We have been passed two arguments: - // First: the SDK key (string) - // Second: the config structure (table) + // 1: the SDK key (string) + // 2: the config structure (table) const char* sdk_key = luaL_checkstring(l, 1); LDServerConfigBuilder builder = LDServerConfigBuilder_New(sdk_key); @@ -739,12 +776,13 @@ makeConfig(lua_State *const l) return out_config; } +// TODO: update the config field names /*** Initialize a new client, and connect to LaunchDarkly. Applications should instantiate a single instance for the lifetime of their application. -@function makeClient +@function initClient @tparam table config list of configuration options @tparam string config.key Environment SDK key -@tparam[opt] string config.baseURI Set the base URI for connecting to +@tparam[opt] string config.serviceEndpoints. Set the base URI for connecting to LaunchDarkly. You probably don't need to set this unless instructed by LaunchDarkly. @tparam[opt] string config.streamURI Set the streaming URI for connecting to diff --git a/redis-test.lua b/redis-test.lua index 599d859..9a8b439 100644 --- a/redis-test.lua +++ b/redis-test.lua @@ -19,6 +19,7 @@ function makeTestClient() dataSystem = { enabled = true, lazyLoad = { + cacheRefreshMilliseconds = 1000, source = r.makeRedisSource('redis://localhost:1234', 'test-prefix') }, } From 56d8432690f6539ac7379d89b5b05cbcfcc8c6e7 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Dec 2023 18:58:43 -0800 Subject: [PATCH 132/161] update redis tests --- redis-test.lua | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/redis-test.lua b/redis-test.lua index 9a8b439..6a6dbb1 100644 --- a/redis-test.lua +++ b/redis-test.lua @@ -35,10 +35,22 @@ function TestAll:tearDown() collectgarbage("collect") end -function TestAll:testRedisBasic() +function TestAll:testVariationWithRedisSource() local e = false u.assertEquals(e, makeTestClient():boolVariation(user, "test", e)) end +function TestAll:testVariationDetailWithRedisSource() + local e = { + value = true, + reason = { + kind = "ERROR", + errorKind = "CLIENT_NOT_READY", + inExperiment = false + } + } + u.assertEquals(makeTestClient():boolVariationDetail(user, "test", true), e) +end + local runner = u.LuaUnit.new() os.exit(runner:runSuite()) From ba01ab0ed380a5dd9a11e7d6f2102a0aff40ac0c Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Dec 2023 19:07:22 -0800 Subject: [PATCH 133/161] more polish --- launchdarkly-server-sdk-redis.c | 4 ++-- launchdarkly-server-sdk.c | 14 ++++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/launchdarkly-server-sdk-redis.c b/launchdarkly-server-sdk-redis.c index 17d859e..246a916 100644 --- a/launchdarkly-server-sdk-redis.c +++ b/launchdarkly-server-sdk-redis.c @@ -43,7 +43,7 @@ LuaLDRedisMakeSource(lua_State *const l) *i = out_result.source; - luaL_getmetatable(l, "LaunchDarklyStoreInterface"); + luaL_getmetatable(l, "LaunchDarklySourceInterface"); lua_setmetatable(l, -2); return 1; @@ -51,7 +51,7 @@ LuaLDRedisMakeSource(lua_State *const l) static const struct luaL_Reg launchdarkly_functions[] = { { "makeRedisSource", LuaLDRedisMakeSource }, - { NULL, NULL } + { NULL, NULL} }; #if !defined LUA_VERSION_NUM || LUA_VERSION_NUM==501 diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index fc56502..27fa91b 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -519,13 +519,11 @@ static void parse_string_array(lua_State *const l, int i, void* builder, void* s } } -// Special purpose parser for grabbing a store interface from a userdata. +// Special purpose parser for extracting a LaunchDarklySourceInterface from +// the stack. Setter must have the signature (void*, void*). static void parse_lazyload_source(lua_State *const l, int i, void* builder, void* setter) { - // TODO: check that this implements the correct userdata. - LDServerLazyLoadSourcePtr *source = lua_touserdata(l, i); - + LDServerLazyLoadSourcePtr *source = luaL_checkudata(l, i, "LaunchDarklySourceInterface"); DEBUG_PRINT("source = %p\n", *source); - void (*source_setter)(void*, void*) = setter; // Dereferencing source because lua_touserdata returns a pointer (to our pointer). @@ -1470,7 +1468,7 @@ static const struct luaL_Reg launchdarkly_user_methods[] = { { NULL, NULL } }; -static const struct luaL_Reg launchdarkly_store_methods[] = { +static const struct luaL_Reg launchdarkly_source_methods[] = { { NULL, NULL } }; @@ -1507,10 +1505,10 @@ luaopen_launchdarkly_server_sdk(lua_State *const l) lua_setfield(l, -2, "__index"); ld_luaL_setfuncs(l, launchdarkly_user_methods, 0); - luaL_newmetatable(l, "LaunchDarklyStoreInterface"); + luaL_newmetatable(l, "LaunchDarklySourceInterface"); lua_pushvalue(l, -1); lua_setfield(l, -2, "__index"); - ld_luaL_setfuncs(l, launchdarkly_store_methods, 0); + ld_luaL_setfuncs(l, launchdarkly_source_methods, 0); #if LUA_VERSION_NUM >= 502 luaL_newlib(l, launchdarkly_functions); From ff407341d25c84260eb695f2d0e44374630281a7 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Dec 2023 19:12:27 -0800 Subject: [PATCH 134/161] test invalid redis argument --- .github/actions/ci/action.yml | 2 +- launchdarkly-server-sdk-redis-1.0-0.rockspec | 2 +- redis-test.lua => test-redis.lua | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) rename redis-test.lua => test-redis.lua (90%) diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index e34180b..c99680b 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -71,7 +71,7 @@ runs: - name: Run Lua Server-side SDK with Redis Tests (JIT) shell: bash if: ${{ contains(inputs.lua-version, 'jit') }} - run: luajit redis-test.lua + run: luajit test-redis.lua env: # Needed because boost isn't installed in default system paths, which is # what the C++ Server-side SDK shared object expects. Same for hiredis which is bundled diff --git a/launchdarkly-server-sdk-redis-1.0-0.rockspec b/launchdarkly-server-sdk-redis-1.0-0.rockspec index 8da7b01..53f0ca0 100644 --- a/launchdarkly-server-sdk-redis-1.0-0.rockspec +++ b/launchdarkly-server-sdk-redis-1.0-0.rockspec @@ -24,7 +24,7 @@ external_dependencies = { test = { type = "command", - script = "redis-test.lua" + script = "test-redis.lua" } build = { diff --git a/redis-test.lua b/test-redis.lua similarity index 90% rename from redis-test.lua rename to test-redis.lua index 6a6dbb1..be70f0b 100644 --- a/redis-test.lua +++ b/test-redis.lua @@ -35,6 +35,10 @@ function TestAll:tearDown() collectgarbage("collect") end +function TestAll:testInvalidRedisArguments() + u.assertErrorMsgContains('invalid URI', r.makeRedisSource, 'not a uri', 'test-prefix') +end + function TestAll:testVariationWithRedisSource() local e = false u.assertEquals(e, makeTestClient():boolVariation(user, "test", e)) From 4c10ba26d540969cf503089ad3f6ac71a2d8c214 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Dec 2023 20:53:01 -0800 Subject: [PATCH 135/161] re-enable more config options in test --- test.lua | 206 +++++++++++++++++++++++++++---------------------------- 1 file changed, 103 insertions(+), 103 deletions(-) diff --git a/test.lua b/test.lua index dbfd98c..15388ff 100644 --- a/test.lua +++ b/test.lua @@ -49,111 +49,111 @@ function TestAll:testSetAllConfigFields() } } }, - --events = { - -- capacity = 1000, - -- --contextKeysCapacity = 100, TODO: add once c binding available - -- enabled = true, - -- flushIntervalMilliseconds = 100, - -- allAttributesPrivate = true, - -- privateAttributes = {"/foo", "/bar"} - --} + events = { + capacity = 1000, + --contextKeysCapacity = 100, TODO: add once c binding available + enabled = true, + flushIntervalMilliseconds = 100, + allAttributesPrivate = true, + privateAttributes = {"/foo", "/bar"} + } }) end --- ---function TestAll:testBoolVariation() --- local e = false --- u.assertEquals(makeTestClient():boolVariation(user, "test", e), e) ---end --- ---function TestAll:testBoolVariationDetail() --- local e = { --- value = true, --- reason = { --- kind = "ERROR", --- errorKind = "FLAG_NOT_FOUND", --- inExperiment = false --- } --- } --- u.assertEquals(makeTestClient():boolVariationDetail(user, "test", true), e) ---end --- ---function TestAll:testIntVariation() --- local e = 3 --- u.assertEquals(makeTestClient():intVariation(user, "test", e), e) ---end --- ---function TestAll:testIntVariationDetail() --- local e = { --- value = 5, --- reason = { --- kind = "ERROR", --- errorKind = "FLAG_NOT_FOUND", --- inExperiment = false --- } --- } --- u.assertEquals(makeTestClient():intVariationDetail(user, "test", 5), e) ---end --- ---function TestAll:testDoubleVariation() --- local e = 12.5 --- u.assertEquals(makeTestClient():doubleVariation(user, "test", e), e) ---end --- ---function TestAll:testDoubleVariationDetail() --- local e = { --- value = 6.2, --- reason = { --- kind = "ERROR", --- errorKind = "FLAG_NOT_FOUND", --- inExperiment = false --- } --- } --- u.assertEquals( makeTestClient():doubleVariationDetail(user, "test", 6.2), e) ---end --- ---function TestAll:testStringVariation() --- local e = "a" --- u.assertEquals(makeTestClient():stringVariation(user, "test", e), e) ---end --- ---function TestAll:testStringVariationDetail() --- local e = { --- value = "f", --- reason = { --- kind = "ERROR", --- errorKind = "FLAG_NOT_FOUND", --- inExperiment = false --- } --- } --- u.assertEquals(makeTestClient():stringVariationDetail(user, "test", "f"), e) ---end --- ---function TestAll:testJSONVariation() --- local e = { ["a"] = "b" } --- u.assertEquals(makeTestClient():jsonVariation(user, "test", e), e) ---end --- ---function TestAll:testJSONVariationDetail() --- local e = { --- value = { a = "b" }, --- reason = { --- kind = "ERROR", --- errorKind = "FLAG_NOT_FOUND", --- inExperiment = false --- } --- } --- u.assertEquals(makeTestClient():jsonVariationDetail(user, "test", { a = "b" }), e) ---end --- ---function TestAll:testIdentify() --- makeTestClient():identify(user) ---end --- ---function TestAll:testVersion() --- local version = l.version() --- u.assertNotIsNil(version) --- u.assertStrMatches(version, "(%d+)%.(%d+)%.(%d+)(.*)") ---end + +function TestAll:testBoolVariation() + local e = false + u.assertEquals(makeTestClient():boolVariation(user, "test", e), e) +end + +function TestAll:testBoolVariationDetail() + local e = { + value = true, + reason = { + kind = "ERROR", + errorKind = "FLAG_NOT_FOUND", + inExperiment = false + } + } + u.assertEquals(makeTestClient():boolVariationDetail(user, "test", true), e) +end + +function TestAll:testIntVariation() + local e = 3 + u.assertEquals(makeTestClient():intVariation(user, "test", e), e) +end + +function TestAll:testIntVariationDetail() + local e = { + value = 5, + reason = { + kind = "ERROR", + errorKind = "FLAG_NOT_FOUND", + inExperiment = false + } + } + u.assertEquals(makeTestClient():intVariationDetail(user, "test", 5), e) +end + +function TestAll:testDoubleVariation() + local e = 12.5 + u.assertEquals(makeTestClient():doubleVariation(user, "test", e), e) +end + +function TestAll:testDoubleVariationDetail() + local e = { + value = 6.2, + reason = { + kind = "ERROR", + errorKind = "FLAG_NOT_FOUND", + inExperiment = false + } + } + u.assertEquals( makeTestClient():doubleVariationDetail(user, "test", 6.2), e) +end + +function TestAll:testStringVariation() + local e = "a" + u.assertEquals(makeTestClient():stringVariation(user, "test", e), e) +end + +function TestAll:testStringVariationDetail() + local e = { + value = "f", + reason = { + kind = "ERROR", + errorKind = "FLAG_NOT_FOUND", + inExperiment = false + } + } + u.assertEquals(makeTestClient():stringVariationDetail(user, "test", "f"), e) +end + +function TestAll:testJSONVariation() + local e = { ["a"] = "b" } + u.assertEquals(makeTestClient():jsonVariation(user, "test", e), e) +end + +function TestAll:testJSONVariationDetail() + local e = { + value = { a = "b" }, + reason = { + kind = "ERROR", + errorKind = "FLAG_NOT_FOUND", + inExperiment = false + } + } + u.assertEquals(makeTestClient():jsonVariationDetail(user, "test", { a = "b" }), e) +end + +function TestAll:testIdentify() + makeTestClient():identify(user) +end + +function TestAll:testVersion() + local version = l.version() + u.assertNotIsNil(version) + u.assertStrMatches(version, "(%d+)%.(%d+)%.(%d+)(.*)") +end local runner = u.LuaUnit.new() os.exit(runner:runSuite()) From 6185b59f8547223c4f3b94dc11d43efccb7dec56 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Dec 2023 21:05:14 -0800 Subject: [PATCH 136/161] add init timeout --- launchdarkly-server-sdk.c | 26 +++++++++++--------------- test-redis.lua | 3 +-- test.lua | 8 ++------ 3 files changed, 14 insertions(+), 23 deletions(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 27fa91b..36fea59 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -19,8 +19,6 @@ Server-side SDK for LaunchDarkly. #include #include -#define DEBUG 1 - #ifdef DEBUG #define DEBUG_PRINT(fmt, ...) printf(fmt, __VA_ARGS__) #else @@ -729,13 +727,8 @@ struct field_validator * find_field(const char *key, struct config* cfg) { } static LDServerConfig -makeConfig(lua_State *const l) +makeConfig(lua_State *const l, const char *const sdk_key) { - // We have been passed two arguments: - // 1: the SDK key (string) - // 2: the config structure (table) - - const char* sdk_key = luaL_checkstring(l, 1); LDServerConfigBuilder builder = LDServerConfigBuilder_New(sdk_key); // Recursively visit the heirarchical configs, modifying the builder @@ -834,17 +827,20 @@ returned. static int LuaLDClientInit(lua_State *const l) { - LDServerSDK client; - LDServerConfig config; - unsigned int timeout; - if (lua_gettop(l) != 2) { - return luaL_error(l, "expecting exactly 2 arguments"); + if (lua_gettop(l) != 3) { + return luaL_error(l, "expecting exactly 3 arguments"); } - config = makeConfig(l); + const char *const sdk_key = luaL_checkstring(l, 1); + + const int timeout = luaL_checkinteger(l, 2); + + LDServerConfig config = makeConfig(l, sdk_key); + + LDServerSDK client = LDServerSDK_New(config); - client = LDServerSDK_New(config); + LDServerSDK_Start(client, timeout, NULL); LDServerSDK *c = (LDServerSDK *) lua_newuserdata(l, sizeof(client)); diff --git a/test-redis.lua b/test-redis.lua index be70f0b..e12e1de 100644 --- a/test-redis.lua +++ b/test-redis.lua @@ -15,7 +15,7 @@ l.registerLogger(logWrite, logEnabled) TestAll = {} function makeTestClient() - local c = l.clientInit("sdk-test", { + return l.clientInit("sdk-test", 0, { dataSystem = { enabled = true, lazyLoad = { @@ -24,7 +24,6 @@ function makeTestClient() }, } }) - return c end local user = l.makeUser({ diff --git a/test.lua b/test.lua index 15388ff..0b38f0c 100644 --- a/test.lua +++ b/test.lua @@ -14,11 +14,7 @@ l.registerLogger(logWrite, logEnabled) TestAll = {} function makeTestClient() - local c = l.clientInit("sdk-test", { - offline = true - }) - - return c + return l.clientInit("sdk-test", 0, { offline = true }) end local user = l.makeUser({ @@ -30,7 +26,7 @@ function TestAll:tearDown() end function TestAll:testSetAllConfigFields() - local c = l.clientInit("sdk-test", { + local c = l.clientInit("sdk-test", 0, { offline = true, appInfo = { identifier = "MyApp", From 9768f99172ace466641080d627fcacb13e734810 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Dec 2023 21:37:09 -0800 Subject: [PATCH 137/161] update docs --- launchdarkly-server-sdk.c | 118 ++++++++++++++++++++------------------ 1 file changed, 63 insertions(+), 55 deletions(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 36fea59..23d2acc 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -75,7 +75,6 @@ void logWrite(enum LDLogLevel level, const char* msg, void *user_data /* ignored /*** -TODO: Document that the log levels have changed and there is an additional callback, and removed the log level arg. Set the global logger for all SDK operations. This function is not thread safe, and if used should be done so before other operations. @function registerLogger @@ -252,7 +251,19 @@ LuaPushJSON(lua_State *const l, LDValue j) } /*** -Create a new opaque context object of kind 'user'. + +Create a new opaque context object of kind 'user'. This method +has changed from the previous Lua SDK v1.x makeUser, as users are no longer +supported. + +Specifically: +- 'secondary' is no longer supported. +- 'privateAttributeNames' is now called 'privateAttributes' and supports +attribute references (similar to JSON pointer syntax, e.g. /foo/bar). +- all fields under 'custom' become top-level context attributes, rather than +being nested under an attribute named 'custom'. For example, { custom = { foo = "bar" } } +would result in a context with attribute 'foo' equal to 'bar'. + @function makeUser @tparam table fields list of user fields. @tparam string fields.key The user's key @@ -264,9 +275,9 @@ Create a new opaque context object of kind 'user'. @tparam[opt] string fields.name Set the user's name @tparam[opt] string fields.avatar Set the user's avatar @tparam[opt] string fields.country Set the user's country -@tparam[opt] table fields.privateAttributeNames A list of attributes to +@tparam[opt] table fields.privateAttributes A list of attributes to redact -@tparam[opt] table fields.custom A table of attributes to set on the user. +@tparam[opt] table fields.custom A table of additional context attributes. @return an opaque context object */ static int @@ -334,15 +345,9 @@ LuaLDUserNew(lua_State *const l) LDContextBuilder_Attributes_Set(builder, "user", "country", LDValue_NewString(luaL_checkstring(l, -1))); } - - - // TODO: Document that secondary was removed - - lua_getfield(l, 1, "custom"); // The individual fields of custom are added to the top-level of the context. - // TODO: document this. if (lua_istable(l, -1)) { lua_pushnil(l); @@ -357,7 +362,7 @@ LuaLDUserNew(lua_State *const l) } } - lua_getfield(l, 1, "privateAttributeNames"); + lua_getfield(l, 1, "privateAttributes"); if (lua_istable(l, -1)) { int n = lua_tablelen(l, -1); @@ -767,61 +772,64 @@ makeConfig(lua_State *const l, const char *const sdk_key) return out_config; } -// TODO: update the config field names /*** -Initialize a new client, and connect to LaunchDarkly. Applications should instantiate a single instance for the lifetime of their application. +Initialize a new client, and connect to LaunchDarkly. +Applications should instantiate a single instance for the lifetime of their application. + @function initClient +@param string Environment SDK key +@param int Initialization timeout in milliseconds. initClient will +block for this long before returning a non-fully initialized client. Pass 0 to return +immediately without waiting (note that the client will continue initializing in the background.) @tparam table config list of configuration options -@tparam string config.key Environment SDK key -@tparam[opt] string config.serviceEndpoints. Set the base URI for connecting to -LaunchDarkly. You probably don't need to set this unless instructed by -LaunchDarkly. -@tparam[opt] string config.streamURI Set the streaming URI for connecting to -LaunchDarkly. You probably don't need to set this unless instructed by -LaunchDarkly. -@tparam[opt] string config.eventsURI Set the events URI for connecting to -LaunchDarkly. You probably don't need to set this unless instructed by +@tparam[opt] boolean config.offline Sets whether this client is offline. +An offline client will not make any network connections to LaunchDarkly or +a data source like Redis, nor send any events, and will return application-defined +default values for all feature flags. +@tparam[opt] table config.serviceEndpoints. If you set one custom service endpoint URL, +you must set all of them. You probably don't need to set this unless instructed by LaunchDarkly. -@tparam[opt] boolean config.stream Enables or disables real-time streaming -flag updates. When set to false, an efficient caching polling mechanism is -used. We do not recommend disabling streaming unless you have been instructed -to do so by LaunchDarkly support. Defaults to true. -@tparam[opt] string config.sendEvents Sets whether to send analytics events +@tparam[opt] string config.serviceEndpoints.streamingBaseURL Set the streaming URL +for connecting to LaunchDarkly. +@tparam[opt] string config.serviceEndpoints.eventsURL Set the events URL for +connecting to LaunchDarkly. +@tparam[opt] string config.serviceEndpoints.pollingURL Set the polling URL for +connecting to LaunchDarkly. +@tparam[opt] table config.events Config options related to event generation and +delivery. +@tparam[opt] bool config.events.enabled Sets whether to send analytics events back to LaunchDarkly. By default, the client will send events. This differs -from Offline in that it only affects sending events, not streaming or -polling. -@tparam[opt] int config.eventsCapacity The capacity of the events buffer. +from top-level config option 'offline' in that it only affects sending events, +not receiving data. +@tparam[opt] int config.events.capacity The capacity of the events buffer. The client buffers up to this many events in memory before flushing. If the capacity is exceeded before the buffer is flushed, events will be discarded. -@tparam[opt] int config.timeout The connection timeout to use when making -requests to LaunchDarkly. -@tparam[opt] int config.flushInterval he time between flushes of the event +@tparam[opt] int config.events.flushIntervalMilliseconds The time between flushes of the event buffer. Decreasing the flush interval means that the event buffer is less likely to reach capacity. -@tparam[opt] int config.pollInterval The polling interval -(when streaming is disabled) in milliseconds. -@tparam[opt] boolean config.offline Sets whether this client is offline. -An offline client will not make any network connections to LaunchDarkly, -and will return default values for all feature flags. -@tparam[opt] boolean config.allAttributesPrivate Sets whether or not all user +@tparam[opt] boolean config.events.allAttributesPrivate Sets whether or not all context attributes (other than the key) should be hidden from LaunchDarkly. If this -is true, all user attribute values will be private, not just the attributes -specified in PrivateAttributeNames. -@tparam[opt] boolean config.inlineUsersInEvents Set to true if you need to -see the full user details in every analytics event. -@tparam[opt] int config.userKeysCapacity The number of user keys that the -event processor can remember at an one time, so that duplicate user details +is true, all context attribute values will be private, not just the attributes +specified in PrivateAttributes. +@tparam[opt] int config.events.contextKeysCapacity The number of context keys that the +event processor can remember at an one time, so that duplicate context details will not be sent in analytics. -@tparam[opt] int config.userKeysFlushInterval The interval at which the event -processor will reset its set of known user keys, in milliseconds. -@tparam[opt] table config.privateAttributeNames Marks a set of user attribute -names private. Any users sent to LaunchDarkly with this configuration active -will have attributes with these names removed. -@param[opt] backend config.featureStoreBackend Persistent feature store -backend. -@tparam int timeoutMilliseconds How long to wait for flags to -download. If the timeout is reached a non fully initialized client will be -returned. +@tparam[opt] table config.events.privateAttributes Marks a set of context attribute +references as private. Any contexts sent to LaunchDarkly with this configuration active +will have attributes refered to removed. +@tparam[opt] table config.appInfo Specify metadata related to your application. +@tparam[opt] string config.appInfo.identifier An identifier for the application. +@tparam[opt] string config.appInfo.version The version of the application. +@tparam[opt] table config.dataSystem Change configuration of the default streaming +data source, switch to a polling source, or specifu a read-only database source. +@tparam[opt] table config.dataSystem.backgroundSync Change streaming or polling +configuration. +@tparam[opt] int config.dataSystem.backgroundSync.streaming.initialReconnectDelayMilliseconds +The time to wait before the first reconnection attempt, if the streaming connection is dropped. +@tparam[opt] int config.dataSystem.lazyLoad.cacheRefreshMilliseconds How long a data item (flag/segment) +remains cached in memory before requiring a refresh from the database. +@tparam[opt] userdata config.dataSystem.lazyLoad.source A custom data source. Currently +only Redis is supported. See @{makeRedisSource}. @return A fresh client. */ static int From e7d8171212418badcc5d3c45c64926aef86de266 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Dec 2023 21:58:37 -0800 Subject: [PATCH 138/161] update docs --- launchdarkly-server-sdk-redis.c | 10 ++++++---- launchdarkly-server-sdk.c | 29 +++++++++++++++++------------ 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/launchdarkly-server-sdk-redis.c b/launchdarkly-server-sdk-redis.c index 246a916..44a0c95 100644 --- a/launchdarkly-server-sdk-redis.c +++ b/launchdarkly-server-sdk-redis.c @@ -15,11 +15,13 @@ Server-side SDK for LaunchDarkly Redis store. /*** Create a Redis data source, which can be used instead -of a LaunchDarkly Streaming or Polling data source. +of a LaunchDarkly Streaming or Polling data source. This should be configured +in the SDK's configuration table, under the dataSystem.lazyLoad.source property. @function makeRedisSource -@tparam string uri Redis URI. -@tparam string prefix Prefix to use when reading SDK data from Redis. -@return A new Redis data source, suitable for configuration in the SDK's data system. +@tparam string uri Redis URI. Example: 'redis://localhost:6379'. +@tparam string prefix Prefix to use when reading SDK data from Redis. This is prefixed to all +Redis keys used by this SDK. Example: 'my-environment'. +@return A new Redis data source. */ static int LuaLDRedisMakeSource(lua_State *const l) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 23d2acc..95dbc19 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -81,7 +81,8 @@ safe, and if used should be done so before other operations. @tparam function writeCb The logging write handler. Callback must be of the form "function (logLevel, logLine) ... end". @tparam function enabledCb The log level enabled handler. Callback must be of the form -"function (logLevel) -> bool ... end". Return true if the given log level is enabled. +"function (logLevel) -> bool ... end". Return true if the given log level is enabled. The +available log levels are 'debug', 'info', 'warn', and 'error'. */ static int LuaLDRegisterLogger(lua_State *const l) @@ -786,7 +787,7 @@ immediately without waiting (note that the client will continue initializing in An offline client will not make any network connections to LaunchDarkly or a data source like Redis, nor send any events, and will return application-defined default values for all feature flags. -@tparam[opt] table config.serviceEndpoints. If you set one custom service endpoint URL, +@tparam[opt] table config.serviceEndpoints If you set one custom service endpoint URL, you must set all of them. You probably don't need to set this unless instructed by LaunchDarkly. @tparam[opt] string config.serviceEndpoints.streamingBaseURL Set the streaming URL @@ -795,7 +796,7 @@ for connecting to LaunchDarkly. connecting to LaunchDarkly. @tparam[opt] string config.serviceEndpoints.pollingURL Set the polling URL for connecting to LaunchDarkly. -@tparam[opt] table config.events Config options related to event generation and +@tparam[opt] table config.events Options related to event generation and delivery. @tparam[opt] bool config.events.enabled Sets whether to send analytics events back to LaunchDarkly. By default, the client will send events. This differs @@ -810,26 +811,30 @@ likely to reach capacity. @tparam[opt] boolean config.events.allAttributesPrivate Sets whether or not all context attributes (other than the key) should be hidden from LaunchDarkly. If this is true, all context attribute values will be private, not just the attributes -specified in PrivateAttributes. +specified via events.privateAttributes. @tparam[opt] int config.events.contextKeysCapacity The number of context keys that the event processor can remember at an one time, so that duplicate context details will not be sent in analytics. @tparam[opt] table config.events.privateAttributes Marks a set of context attribute -references as private. Any contexts sent to LaunchDarkly with this configuration active +as private. Any contexts sent to LaunchDarkly with this configuration active will have attributes refered to removed. @tparam[opt] table config.appInfo Specify metadata related to your application. @tparam[opt] string config.appInfo.identifier An identifier for the application. @tparam[opt] string config.appInfo.version The version of the application. @tparam[opt] table config.dataSystem Change configuration of the default streaming -data source, switch to a polling source, or specifu a read-only database source. +data source, switch to a polling source, or specify a read-only database source. +@tparam[opt] bool config.dataSystem.enabled Set to false to disable receiving any data +from any LaunchDarkly data source (streaming or polling) or a database source (like Redis.) @tparam[opt] table config.dataSystem.backgroundSync Change streaming or polling -configuration. +configuration. The SDK uses streaming by default. Note that streaming and polling are mutually exclusive. @tparam[opt] int config.dataSystem.backgroundSync.streaming.initialReconnectDelayMilliseconds The time to wait before the first reconnection attempt, if the streaming connection is dropped. +@tparam[opt] int config.dataSystem.backgroundSync.polling.intervalSeconds The time between individual +polling requests. @tparam[opt] int config.dataSystem.lazyLoad.cacheRefreshMilliseconds How long a data item (flag/segment) -remains cached in memory before requiring a refresh from the database. +remains cached in memory before requiring a refresh from the source. @tparam[opt] userdata config.dataSystem.lazyLoad.source A custom data source. Currently -only Redis is supported. See @{makeRedisSource}. +only Redis is supported. @return A fresh client. */ static int @@ -1323,7 +1328,7 @@ LuaLDClientFlush(lua_State *const l) } /*** -Reports that a user has performed an event. Custom data, and a metric +Reports that a context has performed an event. Custom data, and a metric can be attached to the event as JSON. @function track @tparam string key The name of the event @@ -1386,7 +1391,7 @@ LuaLDClientIsInitialized(lua_State *const l) } /*** -Generates an identify event for a user. +Generates an identify event for a context. @function identify @tparam context context An opaque context object from @{makeUser} or @{makeContext} @treturn nil @@ -1407,7 +1412,7 @@ LuaLDClientIdentify(lua_State *const l) } /*** -Returns a map from feature flag keys to values for a given user. +Returns a map from feature flag keys to values for a given context. This does not send analytics events back to LaunchDarkly. @function allFlags @tparam context context An opaque context object from @{makeUser} or @{makeContext} From 5b7a0e41fac87845894b0ceec1eee4c801943ee3 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 21 Dec 2023 11:02:19 -0800 Subject: [PATCH 139/161] bump C++ release version --- .github/actions/ci/action.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index c99680b..3f45d6f 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -23,7 +23,7 @@ runs: - name: Install CPP SDK uses: ./.github/actions/install-cpp-sdk-redis with: - version: redis-source-bindings-v1.1.0 + version: redis-source-v1.1.0 path: cpp-sdk - name: Build Lua Server-side SDK @@ -46,7 +46,7 @@ runs: run: luarocks test launchdarkly-server-sdk-1.0-0.rockspec env: # Needed because boost isn't installed in default system paths, which is - # what the C++ Server-side SDK shared objec expects. + # what the C++ Server-side SDK shared object expects. LD_LIBRARY_PATH: ${{ steps.install-boost.outputs.BOOST_ROOT }}/lib - name: Run Lua Server-side SDK Tests (JIT) From 2e6be3537314957408d52516e280e69c27e9d0f6 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 21 Dec 2023 12:01:22 -0800 Subject: [PATCH 140/161] add contextKeysCacheCapacity --- launchdarkly-server-sdk.c | 2 +- test.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 95dbc19..f9fcf1d 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -643,7 +643,7 @@ DEFINE_CONFIG(datasystem_config, "dataSystem", datasystem_fields); struct field_validator event_fields[] = { FIELD("enabled", LUA_TBOOLEAN, parse_bool, LDServerConfigBuilder_Events_Enabled), - FIELD("contextKeysCapacity", LUA_TNUMBER, NULL, NULL), + FIELD("contextKeysCapacity", LUA_TNUMBER, parse_unsigned, LDServerConfigBuilder_Events_ContextKeysCapacity), FIELD("capacity", LUA_TNUMBER, parse_unsigned, LDServerConfigBuilder_Events_Capacity), FIELD("flushIntervalMilliseconds", LUA_TNUMBER, parse_unsigned, LDServerConfigBuilder_Events_FlushIntervalMs), FIELD("allAttributesPrivate", LUA_TBOOLEAN, parse_bool, LDServerConfigBuilder_Events_AllAttributesPrivate), diff --git a/test.lua b/test.lua index 0b38f0c..cf40ed2 100644 --- a/test.lua +++ b/test.lua @@ -47,7 +47,7 @@ function TestAll:testSetAllConfigFields() }, events = { capacity = 1000, - --contextKeysCapacity = 100, TODO: add once c binding available + contextKeysCapacity = 100, enabled = true, flushIntervalMilliseconds = 100, allAttributesPrivate = true, From 280d6c332d63e4460ef45a023ba3499f52b8279c Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 21 Dec 2023 12:35:55 -0800 Subject: [PATCH 141/161] add some unit tests --- test.lua | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test.lua b/test.lua index cf40ed2..f20adca 100644 --- a/test.lua +++ b/test.lua @@ -56,6 +56,36 @@ function TestAll:testSetAllConfigFields() }) end +function TestAll:testImplicitUserContext() + local c = l.makeContext({ + key = "foo" + }) + u.assertEquals(c.kinds, {"user"}) + u.assertEquals(c.canonicalKey, "foo") +end + +function TestAll:testExplicitContextKind() + local c = l.makeContext({ + kind = "device", + key = "foo" + }) + u.assertEquals(c.kinds, {"device"}) + u.assertEquals(c.canonicalKey, "foo") +end + +function TestAll:testMultiKindContext() + local c = l.makeContext({ + device = { + key = "foo" + }, + user = { + key = "bar" + } + }) + u.assertEquals(c.kinds, {"device", "user"}) + u.assertEquals(c.canonicalKey, "device:foo:user:bar") +end + function TestAll:testBoolVariation() local e = false u.assertEquals(makeTestClient():boolVariation(user, "test", e), e) From 909b4b937406aad9d3f7068d5298f4f62fc23c78 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 21 Dec 2023 12:57:41 -0800 Subject: [PATCH 142/161] update tests --- test.lua | 45 ++++++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/test.lua b/test.lua index f20adca..530d44f 100644 --- a/test.lua +++ b/test.lua @@ -58,32 +58,43 @@ end function TestAll:testImplicitUserContext() local c = l.makeContext({ - key = "foo" + user = { + key = "bob", + attributes = { + helmet = { + type = "construction" + } + }, + privateAttributes = { + "/helmet/type" + } + } }) u.assertEquals(c.kinds, {"user"}) - u.assertEquals(c.canonicalKey, "foo") + u.assertEquals(c.canonicalKey, "bob") end -function TestAll:testExplicitContextKind() - local c = l.makeContext({ - kind = "device", - key = "foo" - }) - u.assertEquals(c.kinds, {"device"}) - u.assertEquals(c.canonicalKey, "foo") -end function TestAll:testMultiKindContext() - local c = l.makeContext({ - device = { - key = "foo" - }, + l.makeContext({ user = { - key = "bar" + key = "bob", + attributes = { + age = 42 + }, + privateAttributes = { + "/age" + } + }, + vehicle = { + key = "tractor", + attributes = { + horsepower = 2000 + } } }) - u.assertEquals(c.kinds, {"device", "user"}) - u.assertEquals(c.canonicalKey, "device:foo:user:bar") + u.assertEquals(c.kinds, {"user", "vehicle"}) + u.assertEquals(c.canonicalKey, "user:bob:vehicle:tractor") end function TestAll:testBoolVariation() From 50602c0a8d5a8eed6116b7e1ed6b79eca8a7ea0d Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 21 Dec 2023 13:52:54 -0800 Subject: [PATCH 143/161] begin implementing context parsing logic --- launchdarkly-server-sdk.c | 109 +++++++++++++++++++++++++++++++++++--- test.lua | 17 +++++- 2 files changed, 118 insertions(+), 8 deletions(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index f9fcf1d..54b844d 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -19,6 +19,7 @@ Server-side SDK for LaunchDarkly. #include #include +#define DEBUG 1 #ifdef DEBUG #define DEBUG_PRINT(fmt, ...) printf(fmt, __VA_ARGS__) #else @@ -392,6 +393,102 @@ LuaLDUserNew(lua_State *const l) } +static int +LuaLDContextNew(lua_State *const l) { + + // The single argument is a table containing key/value pairs + // which represent kind/contexts. There is no implicit way of constructing + // a user context - you need to have a 'user = { .. }'. + + if (lua_gettop(l) != 1) { + return luaL_error(l, "expecting exactly 1 argument"); + } + + luaL_checktype(l, 1, LUA_TTABLE); + + LDContextBuilder builder = LDContextBuilder_New(); + + + lua_pushnil(l); + while (lua_next(l, -2) != 0) { + lua_pushvalue(l, -2); + + + int kind_type = lua_type(l, -1); + int context_type = lua_type(l, -2); + + if (kind_type != LUA_TSTRING) { + LDContextBuilder_Free(builder); + luaL_error(l, "top-level table keys must be context kinds; example: user = { ... }"); + } + + if (context_type != LUA_TTABLE) { + LDContextBuilder_Free(builder); + luaL_error(l, "top-level table values must be tables; example: user = { ... }"); + } + + const char* kind = lua_tostring(l, -1); + + DEBUG_PRINT("inspecting %s context\n", kind); + + // The context table is on the top of the stack. It must contain a key. + lua_getfield(l, -2, "key"); + const char *const key = lua_tostring(l, -1); + lua_pop(l, 1); + + if (key == NULL) { + LDContextBuilder_Free(builder); + luaL_error(l, "context kind %s must contain a key", kind); + } + + LDContextBuilder_AddKind(builder, kind, key); + + lua_getfield(l, -2, "attributes"); + int attributes_type = lua_type(l, -1); + if (attributes_type == LUA_TTABLE) { + /* TODO: add each attribute */ + + } else if (attributes_type == LUA_TNIL) { + DEBUG_PRINT("no attributes for %s context\n", kind); + } else { + LDContextBuilder_Free(builder); + luaL_error(l, "context kind %s attributes must be a table", kind); + } + lua_pop(l, 1); + + + lua_getfield(l, -2, "privateAttributes"); + int private_attributes_type = lua_type(l, -1); + if (private_attributes_type == LUA_TTABLE) { + /* TODO: add each private attribute */ + + } else if (private_attributes_type == LUA_TNIL) { + DEBUG_PRINT("no private attributes for %s context\n", kind); + } else { + LDContextBuilder_Free(builder); + luaL_error(l, "context kind %s privateAttributes must be a table", kind); + } + lua_pop(l, 1); + + + lua_pop(l, 2); + } + + lua_pop(l, 1); + + + LDContext context = LDContextBuilder_Build(builder); + + LDContext *u = (LDContext *)lua_newuserdata(l, sizeof(context)); + + *u = context; + + luaL_getmetatable(l, "LaunchDarklyContext"); + lua_setmetatable(l, -2); + return 1; +} + + /*** Return SDK version. @function version @@ -405,11 +502,10 @@ LuaLDVersion(lua_State *const l) } /** -Frees a user object. -@deprecated Users are deprecated. Use contexts instead. +Frees a context object. */ static int -LuaLDUserFree(lua_State *const l) +LuaLDContextFree(lua_State *const l) { LDContext *context; @@ -1447,6 +1543,7 @@ LuaLDClientAllFlags(lua_State *const l) static const struct luaL_Reg launchdarkly_functions[] = { { "clientInit", LuaLDClientInit }, { "makeUser", LuaLDUserNew }, + { "makeContext", LuaLDContextNew }, { "registerLogger", LuaLDRegisterLogger }, { "version", LuaLDVersion }, { NULL, NULL } @@ -1472,8 +1569,8 @@ static const struct luaL_Reg launchdarkly_client_methods[] = { { NULL, NULL } }; -static const struct luaL_Reg launchdarkly_user_methods[] = { - { "__gc", LuaLDUserFree }, +static const struct luaL_Reg launchdarkly_context_methods[] = { + { "__gc", LuaLDContextFree }, { NULL, NULL } }; @@ -1512,7 +1609,7 @@ luaopen_launchdarkly_server_sdk(lua_State *const l) luaL_newmetatable(l, "LaunchDarklyContext"); lua_pushvalue(l, -1); lua_setfield(l, -2, "__index"); - ld_luaL_setfuncs(l, launchdarkly_user_methods, 0); + ld_luaL_setfuncs(l, launchdarkly_context_methods, 0); luaL_newmetatable(l, "LaunchDarklySourceInterface"); lua_pushvalue(l, -1); diff --git a/test.lua b/test.lua index 530d44f..75f5a8d 100644 --- a/test.lua +++ b/test.lua @@ -56,7 +56,7 @@ function TestAll:testSetAllConfigFields() }) end -function TestAll:testImplicitUserContext() +function TestAll:testUserContext() local c = l.makeContext({ user = { key = "bob", @@ -76,7 +76,7 @@ end function TestAll:testMultiKindContext() - l.makeContext({ + local c = l.makeContext({ user = { key = "bob", attributes = { @@ -97,6 +97,19 @@ function TestAll:testMultiKindContext() u.assertEquals(c.canonicalKey, "user:bob:vehicle:tractor") end +function TestAll:testInvalidContexts() + u.assertErrorMsgContains("must be context kinds", l.makeContext, {"foo", "bar"}) + u.assertErrorMsgContains("must be tables", l.makeContext, {foo = 3}) + u.assertErrorMsgContains("expecting exactly", l.makeContext, "foo", "bar") + u.assertErrorMsgContains("table expected", l.makeContext, 3) + u.assertErrorMsgContains("table expected", l.makeContext, "foo") + u.assertErrorMsgContains("device attributes must be a table", l.makeContext, {device = {key = "foo", attributes = 3}}) + u.assertErrorMsgContains("device privateAttributes must be a table", l.makeContext, {device = {key = "foo", privateAttributes = 3}}) + u.assertErrorMsgContains("must contain a key", l.makeContext, {device = {}}) + + +end + function TestAll:testBoolVariation() local e = false u.assertEquals(makeTestClient():boolVariation(user, "test", e), e) From 5acfeaad36585a121e91054c94b72c17240f0a42 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 21 Dec 2023 16:33:48 -0800 Subject: [PATCH 144/161] update tests --- launchdarkly-server-sdk.c | 116 +++++++++++++++++++++++++++++++------- test.lua | 27 ++++++--- 2 files changed, 115 insertions(+), 28 deletions(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 54b844d..3b728fa 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -393,6 +393,10 @@ LuaLDUserNew(lua_State *const l) } +static void parse_private_attrs_or_cleanup(lua_State *const l, LDContextBuilder builder, const char* kind); +static void parse_attrs_or_cleanup(lua_State *const l, LDContextBuilder builder, const char* kind); +static bool field_is_table_or_cleanup(lua_State* const l, int field_type, LDContextBuilder builder, const char* field_name, const char* kind); + static int LuaLDContextNew(lua_State *const l) { @@ -438,39 +442,24 @@ LuaLDContextNew(lua_State *const l) { if (key == NULL) { LDContextBuilder_Free(builder); - luaL_error(l, "context kind %s must contain a key", kind); + luaL_error(l, "context kind %s: must contain key", kind); } LDContextBuilder_AddKind(builder, kind, key); lua_getfield(l, -2, "attributes"); - int attributes_type = lua_type(l, -1); - if (attributes_type == LUA_TTABLE) { - /* TODO: add each attribute */ - - } else if (attributes_type == LUA_TNIL) { - DEBUG_PRINT("no attributes for %s context\n", kind); - } else { - LDContextBuilder_Free(builder); - luaL_error(l, "context kind %s attributes must be a table", kind); + if (field_is_table_or_cleanup(l, lua_type(l, -1), builder, "attributes", kind)) { + parse_attrs_or_cleanup(l, builder, kind); } lua_pop(l, 1); lua_getfield(l, -2, "privateAttributes"); - int private_attributes_type = lua_type(l, -1); - if (private_attributes_type == LUA_TTABLE) { - /* TODO: add each private attribute */ - - } else if (private_attributes_type == LUA_TNIL) { - DEBUG_PRINT("no private attributes for %s context\n", kind); - } else { - LDContextBuilder_Free(builder); - luaL_error(l, "context kind %s privateAttributes must be a table", kind); + if (field_is_table_or_cleanup(l, lua_type(l, -1), builder, "privateAttributes", kind)) { + parse_private_attrs_or_cleanup(l, builder, kind); } lua_pop(l, 1); - lua_pop(l, 2); } @@ -488,6 +477,60 @@ LuaLDContextNew(lua_State *const l) { return 1; } +static bool field_is_table_or_cleanup(lua_State* const l, int field_type, LDContextBuilder builder, const char* field_name, const char* kind) { + if (field_type == LUA_TTABLE) { + DEBUG_PRINT("field %s for %s context is a table\n", field_name, kind); + return true; + } else if (field_type == LUA_TNIL) { + DEBUG_PRINT("no %s for %s context\n", field_name, kind); + } else { + LDContextBuilder_Free(builder); + luaL_error(l, "context kind %s: %s must be a table", kind, field_name); + } + return false; +} + + +static void parse_attrs_or_cleanup(lua_State *const l, LDContextBuilder builder, const char* kind) { + lua_pushnil(l); + while (lua_next(l, -2) != 0) { + lua_pushvalue(l, -2); + + int key_type = lua_type(l, -1); + if (key_type != LUA_TSTRING) { + LDContextBuilder_Free(builder); + luaL_error(l, "context kind %s: top-level attribute keys must be strings", kind); + } + + const char* key = lua_tostring(l, -1); + + DEBUG_PRINT("context kind %s: parsing attribute %s\n", kind, key); + + LDValue value = LuaValueToJSON(l, -2); + + LDContextBuilder_Attributes_Set(builder, kind, key, value); + + lua_pop(l, 2); + } +} + + +static void parse_private_attrs_or_cleanup(lua_State *const l, LDContextBuilder builder, const char* kind) { + int n = lua_tablelen(l, -1); + for (int i = 1; i <= n; i++) { + lua_rawgeti(l, -1, i); + + if (lua_isstring(l, -1)) { + LDContextBuilder_Attributes_AddPrivateAttribute(builder, kind, luaL_checkstring(l, -1)); + } else { + LDContextBuilder_Free(builder); + luaL_error(l, "context kind %s: privateAttributes must be a table of strings", kind); + } + + lua_pop(l, 1); + } +} + /*** Return SDK version. @@ -1070,6 +1113,37 @@ LuaPushDetails(lua_State *const l, LDEvalDetail details, LDValue_Free(value); } +static int +LuaLDContextValid(lua_State *const l) +{ + + LDContext *context = luaL_checkudata(l, 1, "LaunchDarklyContext"); + + lua_pushboolean(l, LDContext_Valid(*context)); + + return 1; +} + +static int +LuaLDContextErrors(lua_State *const l) +{ + if (lua_gettop(l) != 1) { + return luaL_error(l, "expecting exactly 1 argument"); + } + + LDContext *context = luaL_checkudata(l, 1, "LaunchDarklyContext"); + + const char* error = LDContext_Errors(*context); + + if (error) { + lua_pushstring(l, error); + } else { + lua_pushnil(l); + } + + return 1; +} + /** Evaluate a boolean flag @class function @@ -1570,6 +1644,8 @@ static const struct luaL_Reg launchdarkly_client_methods[] = { }; static const struct luaL_Reg launchdarkly_context_methods[] = { + { "valid", LuaLDContextValid }, + { "errors", LuaLDContextErrors }, { "__gc", LuaLDContextFree }, { NULL, NULL } }; diff --git a/test.lua b/test.lua index 75f5a8d..c32e97d 100644 --- a/test.lua +++ b/test.lua @@ -70,8 +70,7 @@ function TestAll:testUserContext() } } }) - u.assertEquals(c.kinds, {"user"}) - u.assertEquals(c.canonicalKey, "bob") + u.assertIsTrue(c:valid()) end @@ -93,20 +92,32 @@ function TestAll:testMultiKindContext() } } }) - u.assertEquals(c.kinds, {"user", "vehicle"}) - u.assertEquals(c.canonicalKey, "user:bob:vehicle:tractor") + u.assertIsTrue(c:valid()) end -function TestAll:testInvalidContexts() +function TestAll:testInvalidContextFormats() u.assertErrorMsgContains("must be context kinds", l.makeContext, {"foo", "bar"}) u.assertErrorMsgContains("must be tables", l.makeContext, {foo = 3}) u.assertErrorMsgContains("expecting exactly", l.makeContext, "foo", "bar") u.assertErrorMsgContains("table expected", l.makeContext, 3) u.assertErrorMsgContains("table expected", l.makeContext, "foo") - u.assertErrorMsgContains("device attributes must be a table", l.makeContext, {device = {key = "foo", attributes = 3}}) - u.assertErrorMsgContains("device privateAttributes must be a table", l.makeContext, {device = {key = "foo", privateAttributes = 3}}) - u.assertErrorMsgContains("must contain a key", l.makeContext, {device = {}}) + u.assertErrorMsgContains("device: attributes must be a table", l.makeContext, {device = {key = "foo", attributes = 3}}) + u.assertErrorMsgContains("device: privateAttributes must be a table", l.makeContext, {device = {key = "foo", privateAttributes = 3}}) + u.assertErrorMsgContains("device: must contain key", l.makeContext, {device = {}}) +end +function TestAll:testInvalidContexts() + local empty_key = l.makeContext({user = {key = ""}}) + local no_kinds = l.makeContext({}) + local invalid_kind_name_multi = l.makeContext({multi = {key = "foo"}}) + local invalid_kind_name_kind = l.makeContext({kind = {key = "foo"}}) + local invalid_kind_chars = l.makeContext({['invalid chars !'] = {key = "foo"}}) + + local invalid_contexts = {empty_key, no_kinds, invalid_kind_name_multi, invalid_kind_name_kind, invalid_kind_chars} + for _, context in ipairs(invalid_contexts) do + u.assertIsFalse(context:valid()) + u.assertNotIsNil(context:errors()) + end end From 7c779c163bb0feaec1d1c00407578ba5ba7f53b6 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 21 Dec 2023 17:11:36 -0800 Subject: [PATCH 145/161] more docs --- launchdarkly-server-sdk.c | 95 ++++++++++++++++++++++++++++++++++----- 1 file changed, 85 insertions(+), 10 deletions(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 3b728fa..598d2fa 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -19,7 +19,6 @@ Server-side SDK for LaunchDarkly. #include #include -#define DEBUG 1 #ifdef DEBUG #define DEBUG_PRINT(fmt, ...) printf(fmt, __VA_ARGS__) #else @@ -254,17 +253,21 @@ LuaPushJSON(lua_State *const l, LDValue j) /*** +This function is deprecated and provided as a convenience. Please transition +to using @{makeContext} instead. + Create a new opaque context object of kind 'user'. This method -has changed from the previous Lua SDK v1.x makeUser, as users are no longer +has changed from the previous Lua SDK v1.x `makeUser`, as users are no longer supported. Specifically: -- 'secondary' is no longer supported. -- 'privateAttributeNames' is now called 'privateAttributes' and supports -attribute references (similar to JSON pointer syntax, e.g. /foo/bar). -- all fields under 'custom' become top-level context attributes, rather than -being nested under an attribute named 'custom'. For example, { custom = { foo = "bar" } } -would result in a context with attribute 'foo' equal to 'bar'. +1. 'secondary' attribute is no longer supported. +2. 'privateAttributeNames' is now called 'privateAttributes' and supports +attribute references (similar to JSON pointer syntax, e.g. `/foo/bar`). +3. all fields under 'custom' become top-level context attributes, rather than +being nested under an attribute named 'custom'. + +For example, `{ custom = { foo = "bar" } }` would result in a context with attribute 'foo' equal to 'bar'. @function makeUser @tparam table fields list of user fields. @@ -397,6 +400,64 @@ static void parse_private_attrs_or_cleanup(lua_State *const l, LDContextBuilder static void parse_attrs_or_cleanup(lua_State *const l, LDContextBuilder builder, const char* kind); static bool field_is_table_or_cleanup(lua_State* const l, int field_type, LDContextBuilder builder, const char* field_name, const char* kind); + +/** + +Create a new opaque context object. This method can be used to create single +or multi-kind contexts. The context's kind must always be specified, even if +it is a user. + +For example, to create a context with a single user kind: +``` +local context = ld.makeContext({ + user = { + key = "alice-123", + attributes = { + name = "alice", + age = 52, + contact = { + email = "alice@mail.com", + phone = "555-555-5555" + } + }, + privateAttributes = { "age", "/contact/phone" } + } +}) +``` + +A multi-kind context can be useful when targeting based on multiple kinds of data. +For example, to associate a device context with a user: + +``` +local context = ld.makeContext({ + user = { + key = "alice-123", + attributes = { + name = "alice" + } + }, + device { + key = "device-123", + attributes = { + manufacturer = "bigcorp" + } + } +}) +``` + +SDK methods will automatically check for context validity. You may check manually +by calling @{valid} to detect errors earlier. + +@function makeContext +@tparam table A table of context kinds, where the table keys are the kind names +and the values are tables containing context's information. +@tparam string [kind.key] The context's key, which is required. +@tparam[opt] [kind.attributes] A table of arbitrary attributes to associate with the context. +@tparam[opt] [kind.privateAttributes] An array of attribute references, indicating which +attributes should be marked private. Attribute references may be simple attribute names +(like 'age'), or may use a JSON-pointer-like syntax (like '/contact/phone'). +@treturn A fresh context. +*/ static int LuaLDContextNew(lua_State *const l) { @@ -412,7 +473,6 @@ LuaLDContextNew(lua_State *const l) { LDContextBuilder builder = LDContextBuilder_New(); - lua_pushnil(l); while (lua_next(l, -2) != 0) { lua_pushvalue(l, -2); @@ -490,7 +550,6 @@ static bool field_is_table_or_cleanup(lua_State* const l, int field_type, LDCont return false; } - static void parse_attrs_or_cleanup(lua_State *const l, LDContextBuilder builder, const char* kind) { lua_pushnil(l); while (lua_next(l, -2) != 0) { @@ -1113,6 +1172,14 @@ LuaPushDetails(lua_State *const l, LDEvalDetail details, LDValue_Free(value); } +/** +Returns true if the context is valid. + +@class function +@name valid +@tparam context context An opaque context object from @{makeUser} or @{makeContext} +@treturn True if valid, otherwise false. +*/ static int LuaLDContextValid(lua_State *const l) { @@ -1124,6 +1191,14 @@ LuaLDContextValid(lua_State *const l) return 1; } +/** +Returns an error string if the context is invalid. + +@class function +@name errors +@tparam context context An opaque context object from @{makeUser} or @{makeContext} +@treturn Error string if valid, otherwise nil. +*/ static int LuaLDContextErrors(lua_State *const l) { From 246d1884061b2987277258a73c786ffbb0b1c8ab Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 21 Dec 2023 17:52:01 -0800 Subject: [PATCH 146/161] add context:canonicalKey --- launchdarkly-server-sdk.c | 31 ++++++++++++++++++++++++++++++- test.lua | 4 +++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 598d2fa..23a71a1 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -1210,7 +1210,7 @@ LuaLDContextErrors(lua_State *const l) const char* error = LDContext_Errors(*context); - if (error) { + if (error && strlen(error) > 0) { lua_pushstring(l, error); } else { lua_pushnil(l); @@ -1219,6 +1219,34 @@ LuaLDContextErrors(lua_State *const l) return 1; } +/** +Returns the canonical key of the context. + +@class function +@name canonicalKey +@tparam context context An opaque context object from @{makeUser} or @{makeContext} +@treturn Canonical key of the context, or nil if the context isn't valid. +*/ +static int +LuaLDContextCanonicalKey(lua_State *const l) +{ + if (lua_gettop(l) != 1) { + return luaL_error(l, "expecting exactly 1 argument"); + } + + LDContext *context = luaL_checkudata(l, 1, "LaunchDarklyContext"); + + const char* key = LDContext_CanonicalKey(*context); + + if (key && strlen(key) > 0) { + lua_pushstring(l, key); + } else { + lua_pushnil(l); + } + + return 1; +} + /** Evaluate a boolean flag @class function @@ -1721,6 +1749,7 @@ static const struct luaL_Reg launchdarkly_client_methods[] = { static const struct luaL_Reg launchdarkly_context_methods[] = { { "valid", LuaLDContextValid }, { "errors", LuaLDContextErrors }, + { "canonicalKey", LuaLDContextCanonicalKey }, { "__gc", LuaLDContextFree }, { NULL, NULL } }; diff --git a/test.lua b/test.lua index c32e97d..ea33123 100644 --- a/test.lua +++ b/test.lua @@ -71,6 +71,7 @@ function TestAll:testUserContext() } }) u.assertIsTrue(c:valid()) + u.assertEquals(c:canonicalKey(), "bob") end @@ -93,6 +94,7 @@ function TestAll:testMultiKindContext() } }) u.assertIsTrue(c:valid()) + u.assertEquals(c:canonicalKey(), "user:bob:vehicle:tractor") end function TestAll:testInvalidContextFormats() @@ -117,8 +119,8 @@ function TestAll:testInvalidContexts() for _, context in ipairs(invalid_contexts) do u.assertIsFalse(context:valid()) u.assertNotIsNil(context:errors()) + u.assertIsNil(context:canonicalKey()) end - end function TestAll:testBoolVariation() From 38aa3817db48148d3967fe6b7b11f2629668cf38 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 21 Dec 2023 18:12:32 -0800 Subject: [PATCH 147/161] add privateAttributes binding --- .github/actions/ci/action.yml | 2 +- launchdarkly-server-sdk.c | 44 +++++++++++++++++++++++++++++++++++ test.lua | 4 ++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index 3f45d6f..fc4c4ed 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -23,7 +23,7 @@ runs: - name: Install CPP SDK uses: ./.github/actions/install-cpp-sdk-redis with: - version: redis-source-v1.1.0 + version: launchdarkly-cpp-server-redis-source-v2.0.0 path: cpp-sdk - name: Build Lua Server-side SDK diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 23a71a1..3aa0785 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -1247,6 +1247,49 @@ LuaLDContextCanonicalKey(lua_State *const l) return 1; } +/** +Returns the private attribute references associated with a particular +context kind. + +@class function +@name privateAttributes +@tparam context context An opaque context object from @{makeUser} or @{makeContext} +@treturn Array of private attribute references, or nil if the kind isn't present in +the context. +*/ +static int +LuaLDContextPrivateAttributes(lua_State *const l) +{ + if (lua_gettop(l) != 2) { + return luaL_error(l, "expecting exactly 2 arguments"); + } + + LDContext *context = luaL_checkudata(l, 1, "LaunchDarklyContext"); + + const char* kind = luaL_checkstring(l, 2); + + LDContext_PrivateAttributesIter iter = LDContext_PrivateAttributesIter_New(*context, kind); + if (iter == NULL) { + lua_pushnil(l); + return 1; + } + + lua_newtable(l); + int count = 1; + while (!LDContext_PrivateAttributesIter_End(iter)) { + const char* attr = LDContext_PrivateAttributesIter_Value(iter); + lua_pushstring(l, attr); + lua_rawseti(l, -2, count); + LDContext_PrivateAttributesIter_Next(iter); + count++; + } + + LDContext_PrivateAttributesIter_Free(iter); + + return 1; +} + + /** Evaluate a boolean flag @class function @@ -1750,6 +1793,7 @@ static const struct luaL_Reg launchdarkly_context_methods[] = { { "valid", LuaLDContextValid }, { "errors", LuaLDContextErrors }, { "canonicalKey", LuaLDContextCanonicalKey }, + { "privateAttributes", LuaLDContextPrivateAttributes }, { "__gc", LuaLDContextFree }, { NULL, NULL } }; diff --git a/test.lua b/test.lua index ea33123..f702d3b 100644 --- a/test.lua +++ b/test.lua @@ -72,6 +72,7 @@ function TestAll:testUserContext() }) u.assertIsTrue(c:valid()) u.assertEquals(c:canonicalKey(), "bob") + u.assertItemsEquals(c:privateAttributes("user"), {"/helmet/type"}) end @@ -95,6 +96,9 @@ function TestAll:testMultiKindContext() }) u.assertIsTrue(c:valid()) u.assertEquals(c:canonicalKey(), "user:bob:vehicle:tractor") + u.assertItemsEquals(c:privateAttributes("user"), {"/age"}) + u.assertItemsEquals(c:privateAttributes("vehicle"), {}) + u.assertIsNil(c:privateAttributes("foo")) end function TestAll:testInvalidContextFormats() From 7616ddaddb9415a95d17912dc0a72cbabcc3b37c Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 21 Dec 2023 18:18:10 -0800 Subject: [PATCH 148/161] add equivalent context tests for variations --- README.md | 2 +- launchdarkly-server-sdk-1.0-0.rockspec | 2 -- launchdarkly-server-sdk-redis-1.0-0.rockspec | 3 --- test-redis.lua | 10 ++++++---- test.lua | 17 +++++++++++++++++ 5 files changed, 24 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 53aaeb8..cfcf0c9 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Supported C server-side SDK versions This version of the Lua server-side SDK depends on the C++ server-side SDK C bindings, as well as the C++ server-side SDK's Redis source integration. -The minimum required version is `1.0.0` for the Redis source. +The minimum required version is `2.0.0` for the Redis source. diff --git a/launchdarkly-server-sdk-1.0-0.rockspec b/launchdarkly-server-sdk-1.0-0.rockspec index ffaa575..fe908e1 100644 --- a/launchdarkly-server-sdk-1.0-0.rockspec +++ b/launchdarkly-server-sdk-1.0-0.rockspec @@ -2,8 +2,6 @@ package = "launchdarkly-server-sdk" rockspec_format = "3.0" --- supported_platforms = {"linux"} - version = "1.0-0" source = { diff --git a/launchdarkly-server-sdk-redis-1.0-0.rockspec b/launchdarkly-server-sdk-redis-1.0-0.rockspec index 53f0ca0..45a7aca 100644 --- a/launchdarkly-server-sdk-redis-1.0-0.rockspec +++ b/launchdarkly-server-sdk-redis-1.0-0.rockspec @@ -2,11 +2,8 @@ package = "launchdarkly-server-sdk-redis" rockspec_format = "3.0" --- supported_platforms = {"linux"} - version = "1.0-0" - source = { url = "git+https://github.com/launchdarkly/lua-server-sdk.git" } diff --git a/test-redis.lua b/test-redis.lua index e12e1de..3c944bb 100644 --- a/test-redis.lua +++ b/test-redis.lua @@ -26,8 +26,10 @@ function makeTestClient() }) end -local user = l.makeUser({ - key = "alice" +local context = l.makeContext({ + user = { + key = "alice" + } }) function TestAll:tearDown() @@ -40,7 +42,7 @@ end function TestAll:testVariationWithRedisSource() local e = false - u.assertEquals(e, makeTestClient():boolVariation(user, "test", e)) + u.assertEquals(e, makeTestClient():boolVariation(context, "test", e)) end function TestAll:testVariationDetailWithRedisSource() @@ -52,7 +54,7 @@ function TestAll:testVariationDetailWithRedisSource() inExperiment = false } } - u.assertEquals(makeTestClient():boolVariationDetail(user, "test", true), e) + u.assertEquals(makeTestClient():boolVariationDetail(context, "test", true), e) end local runner = u.LuaUnit.new() diff --git a/test.lua b/test.lua index f702d3b..a707952 100644 --- a/test.lua +++ b/test.lua @@ -21,6 +21,12 @@ local user = l.makeUser({ key = "alice" }) +local context = l.makeContext({ + user = { + key = "alice" + } +}) + function TestAll:tearDown() collectgarbage("collect") end @@ -130,6 +136,7 @@ end function TestAll:testBoolVariation() local e = false u.assertEquals(makeTestClient():boolVariation(user, "test", e), e) + u.assertEquals(makeTestClient():boolVariation(context, "test", e), e) end function TestAll:testBoolVariationDetail() @@ -142,11 +149,13 @@ function TestAll:testBoolVariationDetail() } } u.assertEquals(makeTestClient():boolVariationDetail(user, "test", true), e) + u.assertEquals(makeTestClient():boolVariationDetail(context, "test", true), e) end function TestAll:testIntVariation() local e = 3 u.assertEquals(makeTestClient():intVariation(user, "test", e), e) + u.assertEquals(makeTestClient():intVariation(context, "test", e), e) end function TestAll:testIntVariationDetail() @@ -159,11 +168,13 @@ function TestAll:testIntVariationDetail() } } u.assertEquals(makeTestClient():intVariationDetail(user, "test", 5), e) + u.assertEquals(makeTestClient():intVariationDetail(context, "test", 5), e) end function TestAll:testDoubleVariation() local e = 12.5 u.assertEquals(makeTestClient():doubleVariation(user, "test", e), e) + u.assertEquals(makeTestClient():doubleVariation(context, "test", e), e) end function TestAll:testDoubleVariationDetail() @@ -176,11 +187,13 @@ function TestAll:testDoubleVariationDetail() } } u.assertEquals( makeTestClient():doubleVariationDetail(user, "test", 6.2), e) + u.assertEquals( makeTestClient():doubleVariationDetail(context, "test", 6.2), e) end function TestAll:testStringVariation() local e = "a" u.assertEquals(makeTestClient():stringVariation(user, "test", e), e) + u.assertEquals(makeTestClient():stringVariation(context, "test", e), e) end function TestAll:testStringVariationDetail() @@ -193,11 +206,13 @@ function TestAll:testStringVariationDetail() } } u.assertEquals(makeTestClient():stringVariationDetail(user, "test", "f"), e) + u.assertEquals(makeTestClient():stringVariationDetail(context, "test", "f"), e) end function TestAll:testJSONVariation() local e = { ["a"] = "b" } u.assertEquals(makeTestClient():jsonVariation(user, "test", e), e) + u.assertEquals(makeTestClient():jsonVariation(context, "test", e), e) end function TestAll:testJSONVariationDetail() @@ -210,10 +225,12 @@ function TestAll:testJSONVariationDetail() } } u.assertEquals(makeTestClient():jsonVariationDetail(user, "test", { a = "b" }), e) + u.assertEquals(makeTestClient():jsonVariationDetail(context, "test", { a = "b" }), e) end function TestAll:testIdentify() makeTestClient():identify(user) + makeTestClient():identify(context) end function TestAll:testVersion() From 4dcee37b9b62f699835eafb729a07d537bf772c0 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 21 Dec 2023 19:54:02 -0800 Subject: [PATCH 149/161] add anonymous and name support --- launchdarkly-server-sdk.c | 78 ++++++++++++++++++++++++++++++++++++++- test.lua | 26 ++++++++++++- 2 files changed, 102 insertions(+), 2 deletions(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 3aa0785..e9e1a4c 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -399,7 +399,8 @@ LuaLDUserNew(lua_State *const l) static void parse_private_attrs_or_cleanup(lua_State *const l, LDContextBuilder builder, const char* kind); static void parse_attrs_or_cleanup(lua_State *const l, LDContextBuilder builder, const char* kind); static bool field_is_table_or_cleanup(lua_State* const l, int field_type, LDContextBuilder builder, const char* field_name, const char* kind); - +static bool field_is_string_or_cleanup(lua_State* const l, int field_type, LDContextBuilder builder, const char* field_name, const char* kind); +static bool field_is_bool_or_cleanup(lua_State* const l, int field_type, LDContextBuilder builder, const char* field_name, const char* kind); /** @@ -520,6 +521,19 @@ LuaLDContextNew(lua_State *const l) { } lua_pop(l, 1); + lua_getfield(l, -2, "name"); + if (field_is_string_or_cleanup(l, lua_type(l, -1), builder, "name", kind)) { + LDContextBuilder_Attributes_SetName(builder, kind, lua_tostring(l, -1)); + } + lua_pop(l, 1); + + lua_getfield(l, -2, "anonymous"); + if (field_is_bool_or_cleanup(l, lua_type(l, -1), builder, "anonymous", kind)) { + LDContextBuilder_Attributes_SetAnonymous(builder, kind, lua_toboolean(l, -1)); + } + lua_pop(l, 1); + + lua_pop(l, 2); } @@ -550,6 +564,32 @@ static bool field_is_table_or_cleanup(lua_State* const l, int field_type, LDCont return false; } +static bool field_is_bool_or_cleanup(lua_State* const l, int field_type, LDContextBuilder builder, const char* field_name, const char* kind) { + if (field_type == LUA_TBOOLEAN) { + DEBUG_PRINT("field %s for %s context is a boolean\n", field_name, kind); + return true; + } else if (field_type == LUA_TNIL) { + DEBUG_PRINT("no %s for %s context\n", field_name, kind); + } else { + LDContextBuilder_Free(builder); + luaL_error(l, "context kind %s: %s must be a boolean", kind, field_name); + } + return false; +} + +static bool field_is_string_or_cleanup(lua_State* const l, int field_type, LDContextBuilder builder, const char* field_name, const char* kind) { + if (field_type == LUA_TSTRING) { + DEBUG_PRINT("field %s for %s context is a string\n", field_name, kind); + return true; + } else if (field_type == LUA_TNIL) { + DEBUG_PRINT("no %s for %s context\n", field_name, kind); + } else { + LDContextBuilder_Free(builder); + luaL_error(l, "context kind %s: %s must be a string", kind, field_name); + } + return false; +} + static void parse_attrs_or_cleanup(lua_State *const l, LDContextBuilder builder, const char* kind) { lua_pushnil(l); while (lua_next(l, -2) != 0) { @@ -1247,6 +1287,40 @@ LuaLDContextCanonicalKey(lua_State *const l) return 1; } +/** +Returns the value of a context attribute. + +@class function +@name getAttribute +@tparam context context An opaque context object from @{makeUser} or @{makeContext} +@tparam string kind The kind of the context. +@tparam string attribute_reference An attribute reference naming the attribute to get. +@treturn +*/ +static int +LuaLDContextGetAttribute(lua_State *const l) +{ + if (lua_gettop(l) != 3) { + return luaL_error(l, "expecting exactly 3 arguments"); + } + + LDContext *context = luaL_checkudata(l, 1, "LaunchDarklyContext"); + const char *const kind = luaL_checkstring(l, 2); + const char *const attribute_reference = luaL_checkstring(l, 3); + + LDValue val = LDContext_Get(*context, kind, attribute_reference); + + if (val == NULL) { + lua_pushnil(l); + return 1; + } + + LuaPushJSON(l, val); + + /* Don't need to free the value as it's owned by the context. */ + return 1; +} + /** Returns the private attribute references associated with a particular context kind. @@ -1254,6 +1328,7 @@ context kind. @class function @name privateAttributes @tparam context context An opaque context object from @{makeUser} or @{makeContext} +@tparam string kind The kind of the context. @treturn Array of private attribute references, or nil if the kind isn't present in the context. */ @@ -1794,6 +1869,7 @@ static const struct luaL_Reg launchdarkly_context_methods[] = { { "errors", LuaLDContextErrors }, { "canonicalKey", LuaLDContextCanonicalKey }, { "privateAttributes", LuaLDContextPrivateAttributes }, + { "getAttribute", LuaLDContextGetAttribute }, { "__gc", LuaLDContextFree }, { NULL, NULL } }; diff --git a/test.lua b/test.lua index a707952..f3e0e85 100644 --- a/test.lua +++ b/test.lua @@ -66,6 +66,8 @@ function TestAll:testUserContext() local c = l.makeContext({ user = { key = "bob", + name = "Bob", + anonymous = true, attributes = { helmet = { type = "construction" @@ -79,6 +81,10 @@ function TestAll:testUserContext() u.assertIsTrue(c:valid()) u.assertEquals(c:canonicalKey(), "bob") u.assertItemsEquals(c:privateAttributes("user"), {"/helmet/type"}) + u.assertEquals(c:getAttribute("user", "key"), "bob") + u.assertEquals(c:getAttribute("user", "name"), "Bob") + u.assertEquals(c:getAttribute("user", "anonymous"), true) + u.assertEquals(c:getAttribute("user", "helmet"), {type = "construction"}) end @@ -102,9 +108,19 @@ function TestAll:testMultiKindContext() }) u.assertIsTrue(c:valid()) u.assertEquals(c:canonicalKey(), "user:bob:vehicle:tractor") + u.assertItemsEquals(c:privateAttributes("user"), {"/age"}) u.assertItemsEquals(c:privateAttributes("vehicle"), {}) + u.assertIsNil(c:privateAttributes("foo")) + + u.assertEquals(c:getAttribute("user", "key"), "bob") + u.assertEquals(c:getAttribute("user", "age"), 42) + u.assertEquals(c:getAttribute("vehicle", "key"), "tractor") + u.assertEquals(c:getAttribute("vehicle", "horsepower"), 2000) + + u.assertIsNil(c:getAttribute("foo", "bar")) + u.assertIsNil(c:getAttribute("user", "nonexistent")) end function TestAll:testInvalidContextFormats() @@ -116,6 +132,8 @@ function TestAll:testInvalidContextFormats() u.assertErrorMsgContains("device: attributes must be a table", l.makeContext, {device = {key = "foo", attributes = 3}}) u.assertErrorMsgContains("device: privateAttributes must be a table", l.makeContext, {device = {key = "foo", privateAttributes = 3}}) u.assertErrorMsgContains("device: must contain key", l.makeContext, {device = {}}) + u.assertErrorMsgContains("anonymous must be a boolean", l.makeContext, {user = { key = "foo", anonymous = 3}}) + u.assertErrorMsgContains("name must be a string", l.makeContext, {user = { key = "foo", name = true }}) end function TestAll:testInvalidContexts() @@ -125,7 +143,13 @@ function TestAll:testInvalidContexts() local invalid_kind_name_kind = l.makeContext({kind = {key = "foo"}}) local invalid_kind_chars = l.makeContext({['invalid chars !'] = {key = "foo"}}) - local invalid_contexts = {empty_key, no_kinds, invalid_kind_name_multi, invalid_kind_name_kind, invalid_kind_chars} + local invalid_contexts = { + empty_key, + no_kinds, + invalid_kind_name_multi, + invalid_kind_name_kind, + invalid_kind_chars + } for _, context in ipairs(invalid_contexts) do u.assertIsFalse(context:valid()) u.assertNotIsNil(context:errors()) From 8e41672a7d44743429746fdca6e7e3b46f4c1ccc Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 26 Dec 2023 13:55:26 -0800 Subject: [PATCH 150/161] implement allFlags --- launchdarkly-server-sdk.c | 9 +++------ test-redis.lua | 7 +++++++ test.lua | 6 ++++++ 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index e9e1a4c..255710d 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -1822,16 +1822,13 @@ LuaLDClientAllFlags(lua_State *const l) LDAllFlagsState state = LDServerSDK_AllFlagsState(*client, *context, LD_ALLFLAGSSTATE_DEFAULT); - char* serialized = LDAllFlagsState_SerializeJSON(state); + LDValue owned_map = LDAllFlagsState_Map(state); - /** TODO: Need to add a C binding to expose this as an LDValue, or have an iterator specific to it. **/ - LDMemory_FreeString(serialized); + LuaPushJSON(l, owned_map); + LDValue_Free(owned_map); LDAllFlagsState_Free(state); - /* For now, return an empty table. */ - lua_newtable(l); - return 1; } diff --git a/test-redis.lua b/test-redis.lua index 3c944bb..c10d519 100644 --- a/test-redis.lua +++ b/test-redis.lua @@ -22,6 +22,9 @@ function makeTestClient() cacheRefreshMilliseconds = 1000, source = r.makeRedisSource('redis://localhost:1234', 'test-prefix') }, + }, + events = { + enabled = false } }) end @@ -57,5 +60,9 @@ function TestAll:testVariationDetailWithRedisSource() u.assertEquals(makeTestClient():boolVariationDetail(context, "test", true), e) end +function TestAll:testAllFlags() + u.assertEquals(makeTestClient():allFlags(context), {}) +end + local runner = u.LuaUnit.new() os.exit(runner:runSuite()) diff --git a/test.lua b/test.lua index f3e0e85..b42201a 100644 --- a/test.lua +++ b/test.lua @@ -257,6 +257,12 @@ function TestAll:testIdentify() makeTestClient():identify(context) end + +function TestAll:testAllFlags() + u.assertEquals(makeTestClient():allFlags(user), {}) + u.assertEquals(makeTestClient():allFlags(context), {}) +end + function TestAll:testVersion() local version = l.version() u.assertNotIsNil(version) From e11d160a705bdaf8f792ac745cabe935fb422aae Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 26 Dec 2023 14:54:15 -0800 Subject: [PATCH 151/161] bump redis source version --- .github/actions/ci/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index fc4c4ed..ecd0256 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -23,7 +23,7 @@ runs: - name: Install CPP SDK uses: ./.github/actions/install-cpp-sdk-redis with: - version: launchdarkly-cpp-server-redis-source-v2.0.0 + version: launchdarkly-cpp-server-redis-source-v2.1.0 path: cpp-sdk - name: Build Lua Server-side SDK From c74c247454d5cb094594f4f879a53ea61b8dcb25 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 26 Dec 2023 16:10:30 -0800 Subject: [PATCH 152/161] update min version in readme --- README.md | 2 +- launchdarkly-server-sdk.c | 7 +++++-- test.lua | 4 ++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cfcf0c9..3d3f4e4 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Supported C server-side SDK versions This version of the Lua server-side SDK depends on the C++ server-side SDK C bindings, as well as the C++ server-side SDK's Redis source integration. -The minimum required version is `2.0.0` for the Redis source. +The minimum required version is `2.1.0` for the Redis source. diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 255710d..f121b72 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -39,8 +39,8 @@ LuaArrayToJSON(lua_State *const l, const int i); static void LuaPushJSON(lua_State *const l, LDValue j); -static int globalLogEnabledCallback; -static int globalLogWriteCallback; +static int globalLogEnabledCallback = LUA_NOREF; +static int globalLogWriteCallback = LUA_NOREF; static lua_State *globalLuaState; @@ -457,6 +457,9 @@ and the values are tables containing context's information. @tparam[opt] [kind.privateAttributes] An array of attribute references, indicating which attributes should be marked private. Attribute references may be simple attribute names (like 'age'), or may use a JSON-pointer-like syntax (like '/contact/phone'). +@tparam[opt] [kind.name] A name for the context. This is useful for identifying the context +in the LaunchDarkly dashboard. +@tparam[opt] [kind.anonymous] A boolean indicating whether the context should be marked as anonymous. @treturn A fresh context. */ static int diff --git a/test.lua b/test.lua index b42201a..991d5b9 100644 --- a/test.lua +++ b/test.lua @@ -31,6 +31,10 @@ function TestAll:tearDown() collectgarbage("collect") end +function TestAll:testSetNoConfigFields() + u.assertNotIsNil(l.clientInit("sdk-test", 0, {})) +end + function TestAll:testSetAllConfigFields() local c = l.clientInit("sdk-test", 0, { offline = true, From 96376c11d795e48e80d1fd1ebf86541891d16276 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 26 Dec 2023 16:37:08 -0800 Subject: [PATCH 153/161] add log level config --- launchdarkly-server-sdk.c | 36 +++++++++++++++++++++++++++++++++++- test.lua | 4 ++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index f121b72..1b497d0 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -703,6 +703,21 @@ static void parse_string(lua_State *const l, int i, void* builder, void* setter) string_setter(builder, value); } +static void parse_log_level(lua_State *const l, int i, void* builder, void* setter) { + const char *const value = lua_tostring(l, i); + DEBUG_PRINT("log level = %s\n", value ? value : "NULL"); + void (*log_level_setter)(void*, enum LDLogLevel) = setter; + + // Issue an error if the log level isn't known. Another option would be + // to silently default to a known level, such as LD_LOG_INFO. + const int unknown_level_sentinel = 255; + enum LDLogLevel level = LDLogLevel_Enum(value, 255); + if (level == unknown_level_sentinel) { + luaL_error(l, "unknown log level: '%s' (known options include 'debug', 'info', 'warn', and 'error')", value); + } + log_level_setter(builder, level); +} + // Parses a bool. // The setter must have the signature (void*, bool). static void parse_bool(lua_State *const l, int i, void* builder, void* setter) { @@ -908,12 +923,27 @@ struct field_validator appinfo_fields[] = { DEFINE_CONFIG(appinfo_config, "appInfo", appinfo_fields); + +struct field_validator logging_fields[] = { + FIELD("level", LUA_TSTRING, parse_log_level, LDLoggingBasicBuilder_Level), + FIELD("tag", LUA_TSTRING, parse_string, LDLoggingBasicBuilder_Tag) +}; + +DEFINE_SUB_CONFIG( + logging_config, + "logging", + logging_fields, + LDLoggingBasicBuilder_New, + LDServerConfigBuilder_Logging_Basic +); + struct field_validator top_level_fields[] = { FIELD("appInfo", LUA_TTABLE, parse_table, &appinfo_config), FIELD("serviceEndpoints", LUA_TTABLE, parse_table, &endpoint_config), FIELD("offline", LUA_TBOOLEAN, parse_bool, LDServerConfigBuilder_Offline), FIELD("dataSystem", LUA_TTABLE, parse_table, &datasystem_config), - FIELD("events", LUA_TTABLE, parse_table, &event_config) + FIELD("events", LUA_TTABLE, parse_table, &event_config), + FIELD("logging", LUA_TTABLE, parse_table, &logging_config) }; DEFINE_CONFIG(top_level_config, "config", top_level_fields); @@ -1028,6 +1058,10 @@ immediately without waiting (note that the client will continue initializing in An offline client will not make any network connections to LaunchDarkly or a data source like Redis, nor send any events, and will return application-defined default values for all feature flags. +@tparam[opt] table config.logging Options related to the SDK's logging facilities. +@tparam[opt] string config.logging.tag A tag to include in log messages, for example 'launchdarkly'. +@tparam[opt] string config.logging.level The minimum level of log messages to include. Known options include +'debug', 'info', 'warn', or 'error'. @tparam[opt] table config.serviceEndpoints If you set one custom service endpoint URL, you must set all of them. You probably don't need to set this unless instructed by LaunchDarkly. diff --git a/test.lua b/test.lua index 991d5b9..37e21e2 100644 --- a/test.lua +++ b/test.lua @@ -62,6 +62,10 @@ function TestAll:testSetAllConfigFields() flushIntervalMilliseconds = 100, allAttributesPrivate = true, privateAttributes = {"/foo", "/bar"} + }, + logging = { + level = "debug", + tag = "LaunchDarklyLuaTest" } }) end From b214285aa920ce4757768d6831d6229a6ba9da02 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 27 Dec 2023 12:30:19 -0800 Subject: [PATCH 154/161] add custom logging support --- launchdarkly-server-sdk.c | 178 +++++++++++++++++++++++++------------- test-redis.lua | 16 ++-- test.lua | 34 +++++--- 3 files changed, 143 insertions(+), 85 deletions(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 1b497d0..f73bb10 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -39,11 +39,6 @@ LuaArrayToJSON(lua_State *const l, const int i); static void LuaPushJSON(lua_State *const l, LDValue j); -static int globalLogEnabledCallback = LUA_NOREF; -static int globalLogWriteCallback = LUA_NOREF; - -static lua_State *globalLuaState; - static int lua_tablelen(lua_State *L, int index) { #if LUA_VERSION_NUM >= 502 @@ -53,51 +48,89 @@ static int lua_tablelen(lua_State *L, int index) #endif } +/** +* The custom log backend struct (LDLogBackend) contains a void* userdata pointer, and two function pointers +* (enabled and write). This allows C users to avoid global logging functions. + +* This doesn't quite match up with Lua's expectations, since: +* 1) The user-defined functions will be stored in the Lua registry +* 2) We need to keep around a lua_State pointer for accessing the registry when we need to invoke the callbacks +* +* This struct is therefore the equivalent of the C LDLogBackend struct, but with registry references and a lua_State pointer. +* We allocate a new Lua userdata and then wire up some global callbacks (lua_log_backend_enabled, lua_log_backend_write) +* to cast the void* to a lua_log_backend*, grab the function references, and forward the arguments. +*/ +struct lua_log_backend { + lua_State *l; + int enabled_ref; + int write_ref; +}; + +/** +* Creates a new custom log backend. The functions provided must be thread safe as the SDK +* does not perform any locking. +* @function makeLogBackend +* @tparam function enabled A function that returns true if the specified log level is enabled. The signature +should be (level: string) -> boolean, where the known level strings are 'debug', 'info', 'warn', and 'error'. +* @tparam function write A function that writes a log message at a specified level. The signature should be +* (level: string, message: string) -> void. +* @treturn A new custom log backend, suitable for use in SDK `logging` configuration. +*/ +static int LuaLDLogBackendNew(lua_State *l) { + + if (lua_gettop(l) != 2) { + return luaL_error(l, "expecting exactly 2 arguments"); + } -bool logEnabled(enum LDLogLevel level, void *user_data /* ignored */) { - lua_rawgeti(globalLuaState, LUA_REGISTRYINDEX, globalLogEnabledCallback); + luaL_checktype(l, 1, LUA_TFUNCTION); + luaL_checktype(l, 2, LUA_TFUNCTION); - lua_pushstring(globalLuaState, LDLogLevel_Name(level, "unknown")); + int write_ref = luaL_ref(l, LUA_REGISTRYINDEX); + int enabled_ref = luaL_ref(l, LUA_REGISTRYINDEX); - lua_call(globalLuaState, 1, 1); // one argument (level, string), one result (enable, boolean). + struct lua_log_backend* backend = lua_newuserdata(l, sizeof(struct lua_log_backend)); + luaL_getmetatable(l, "LaunchDarklyLogBackend"); + lua_setmetatable(l, -2); + + backend->l = l; + backend->write_ref = write_ref; + backend->enabled_ref = enabled_ref; - return lua_toboolean(globalLuaState, -1); + return 1; } -void logWrite(enum LDLogLevel level, const char* msg, void *user_data /* ignored */) { - lua_rawgeti(globalLuaState, LUA_REGISTRYINDEX, globalLogWriteCallback); +static struct lua_log_backend* check_log_backend(lua_State *l, int i) { + void *ud = luaL_checkudata(l, i, "LaunchDarklyLogBackend"); + luaL_argcheck(l, ud != NULL, i, "`LaunchDarklyLogBackend' expected"); + return ud; +} + +bool lua_log_backend_enabled(enum LDLogLevel level, void *user_data) { + struct lua_log_backend* backend = user_data; + lua_State *l = backend->l; + + lua_rawgeti(l, LUA_REGISTRYINDEX, backend->enabled_ref); + + lua_pushstring(l, LDLogLevel_Name(level, "unknown")); - lua_pushstring(globalLuaState, LDLogLevel_Name(level, "unknown")); - lua_pushstring(globalLuaState, msg); + lua_call(l, 1, 1); // one argument (level - string), one result (enabled - boolean). - lua_call(globalLuaState, 2, 0); // two args (level, string + msg, string), no results. + return lua_toboolean(l, -1); } +void lua_log_backend_write(enum LDLogLevel level, const char* msg, void *user_data) { + struct lua_log_backend* backend = user_data; + lua_State *l = backend->l; -/*** -Set the global logger for all SDK operations. This function is not thread -safe, and if used should be done so before other operations. -@function registerLogger -@tparam function writeCb The logging write handler. Callback must be of the form -"function (logLevel, logLine) ... end". -@tparam function enabledCb The log level enabled handler. Callback must be of the form -"function (logLevel) -> bool ... end". Return true if the given log level is enabled. The -available log levels are 'debug', 'info', 'warn', and 'error'. -*/ -static int -LuaLDRegisterLogger(lua_State *const l) -{ - if (lua_gettop(l) != 2) { - return luaL_error(l, "expecting exactly 2 arguments"); - } + lua_rawgeti(l, LUA_REGISTRYINDEX, backend->write_ref); - globalLuaState = l; - globalLogEnabledCallback = luaL_ref(l, LUA_REGISTRYINDEX); - globalLogWriteCallback = luaL_ref(l, LUA_REGISTRYINDEX); + lua_pushstring(l, LDLogLevel_Name(level, "unknown")); + lua_pushstring(l, msg); - return 0; + lua_call(l, 2, 0); // two args (level - string, msg - string), no results. } + static bool isArray(lua_State *const l, const int i) { @@ -251,6 +284,7 @@ LuaPushJSON(lua_State *const l, LDValue j) return; } + /*** This function is deprecated and provided as a convenience. Please transition @@ -790,6 +824,23 @@ static void parse_lazyload_source(lua_State *const l, int i, void* builder, void source_setter(builder, *source); } + +static void parse_log_backend(lua_State *const l, int i, void* builder, void* setter) { + struct LDLogBackend backend; + LDLogBackend_Init(&backend); + + struct lua_log_backend* lua_backend = check_log_backend(l, i); + backend.UserData = lua_backend; + backend.Enabled = lua_log_backend_enabled; + backend.Write = lua_log_backend_write; + + LDLoggingCustomBuilder custom_logging = LDLoggingCustomBuilder_New(); + LDLoggingCustomBuilder_Backend(custom_logging, backend); + + void (*backend_setter)(void*, LDLoggingCustomBuilder) = setter; + backend_setter(builder, custom_logging); +} + // Function that returns a new child builder. This is used to allocate builders // which are necessary for building a child config. This is needed when the C++ SDK's // API requires a builder to be allocated and passed in to the top-level builder, e.g. @@ -923,20 +974,28 @@ struct field_validator appinfo_fields[] = { DEFINE_CONFIG(appinfo_config, "appInfo", appinfo_fields); - -struct field_validator logging_fields[] = { +struct field_validator basic_logging_fields[] = { FIELD("level", LUA_TSTRING, parse_log_level, LDLoggingBasicBuilder_Level), FIELD("tag", LUA_TSTRING, parse_string, LDLoggingBasicBuilder_Tag) }; DEFINE_SUB_CONFIG( - logging_config, - "logging", - logging_fields, + basic_logging_config, + "logging.basic", + basic_logging_fields, LDLoggingBasicBuilder_New, LDServerConfigBuilder_Logging_Basic ); + +struct field_validator logging_fields[] = { + /* These are mutually exclusive */ + FIELD("basic", LUA_TTABLE, parse_table, &basic_logging_config), + FIELD("custom", LUA_TUSERDATA, parse_log_backend, LDServerConfigBuilder_Logging_Custom) +}; + +DEFINE_CONFIG(logging_config, "logging", logging_fields); + struct field_validator top_level_fields[] = { FIELD("appInfo", LUA_TTABLE, parse_table, &appinfo_config), FIELD("serviceEndpoints", LUA_TTABLE, parse_table, &endpoint_config), @@ -1012,24 +1071,6 @@ makeConfig(lua_State *const l, const char *const sdk_key) // as we go along. traverse_config(l, builder, &top_level_config); - bool logging_callbacks_set = - globalLogEnabledCallback != LUA_NOREF && - globalLogWriteCallback != LUA_NOREF; - - DEBUG_PRINT("logging callbacks set? %s\n", logging_callbacks_set ? "true" : "false"); - - if (logging_callbacks_set) { - struct LDLogBackend backend; - LDLogBackend_Init(&backend); - - backend.Write = logWrite; - backend.Enabled = logEnabled; - - LDLoggingCustomBuilder custom_logging = LDLoggingCustomBuilder_New(); - LDLoggingCustomBuilder_Backend(custom_logging, backend); - LDServerConfigBuilder_Logging_Custom(builder, custom_logging); - } - LDServerConfigBuilder_HttpProperties_WrapperName(builder, "lua-server-sdk"); LDServerConfigBuilder_HttpProperties_WrapperVersion(builder, SDKVersion); @@ -1058,10 +1099,14 @@ immediately without waiting (note that the client will continue initializing in An offline client will not make any network connections to LaunchDarkly or a data source like Redis, nor send any events, and will return application-defined default values for all feature flags. -@tparam[opt] table config.logging Options related to the SDK's logging facilities. -@tparam[opt] string config.logging.tag A tag to include in log messages, for example 'launchdarkly'. -@tparam[opt] string config.logging.level The minimum level of log messages to include. Known options include +@tparam[opt] table config.logging Options related to the SDK's logging facilities. The `basic` and `custom` configs +are mutually exclusive. If you only need to change logging verbosity or the log tag, use `basic`. If you need to +replace the log verbosity logic & printing entirely, use `custom`. +@tparam[opt] table config.logging.basic Modify the SDK's default logger. +@tparam[opt] string config.logging.basic.tag A tag to include in log messages, for example 'launchdarkly'. +@tparam[opt] string config.logging.basic.level The minimum level of log messages to include. Known options include 'debug', 'info', 'warn', or 'error'. +@tparam[opt] userdata config.logging.custom A custom log backend, created with @{newLogBackend}. @tparam[opt] table config.serviceEndpoints If you set one custom service endpoint URL, you must set all of them. You probably don't need to set this unless instructed by LaunchDarkly. @@ -1873,8 +1918,8 @@ static const struct luaL_Reg launchdarkly_functions[] = { { "clientInit", LuaLDClientInit }, { "makeUser", LuaLDUserNew }, { "makeContext", LuaLDContextNew }, - { "registerLogger", LuaLDRegisterLogger }, { "version", LuaLDVersion }, + { "makeLogBackend", LuaLDLogBackendNew }, { NULL, NULL } }; @@ -1912,6 +1957,10 @@ static const struct luaL_Reg launchdarkly_source_methods[] = { { NULL, NULL } }; +static const struct luaL_Reg launchdarkly_log_backend_methods[] = { + { NULL, NULL } +}; + /* ** Adapted from Lua 5.2.0 */ @@ -1950,6 +1999,11 @@ luaopen_launchdarkly_server_sdk(lua_State *const l) lua_setfield(l, -2, "__index"); ld_luaL_setfuncs(l, launchdarkly_source_methods, 0); + luaL_newmetatable(l, "LaunchDarklyLogBackend"); + lua_pushvalue(l, -1); + lua_setfield(l, -2, "__index"); + ld_luaL_setfuncs(l, launchdarkly_log_backend_methods, 0); + #if LUA_VERSION_NUM >= 502 luaL_newlib(l, launchdarkly_functions); #else diff --git a/test-redis.lua b/test-redis.lua index c10d519..786482c 100644 --- a/test-redis.lua +++ b/test-redis.lua @@ -2,16 +2,6 @@ local u = require('luaunit') local l = require("launchdarkly_server_sdk") local r = require("launchdarkly_server_sdk_redis") -function logWrite(level, line) - print("[" .. level .. "]" .. " " .. line) -end - -function logEnabled(level) - return level == "warn" or level == "error" -end - -l.registerLogger(logWrite, logEnabled) - TestAll = {} function makeTestClient() @@ -25,6 +15,12 @@ function makeTestClient() }, events = { enabled = false + }, + logging = { + basic = { + tag = "LaunchDarklyLua", + level = "warn" + } } }) end diff --git a/test.lua b/test.lua index 37e21e2..4e44a15 100644 --- a/test.lua +++ b/test.lua @@ -1,20 +1,23 @@ local u = require('luaunit') local l = require("launchdarkly_server_sdk") -function logWrite(level, line) - print("[" .. level .. "]" .. " " .. line) -end - -function logEnabled(level) - return level == "warn" or level == "error" -end - -l.registerLogger(logWrite, logEnabled) - TestAll = {} function makeTestClient() - return l.clientInit("sdk-test", 0, { offline = true }) + return l.clientInit("sdk-test", 0, -- nonblocking + { + offline = true, + logging = { + custom = l.makeLogBackend( + function(level) + -- log everything + return true end, + function(level, line) + print(string.format("[%s] %s", level, line)) + end + ), + } + }) end local user = l.makeUser({ @@ -35,6 +38,7 @@ function TestAll:testSetNoConfigFields() u.assertNotIsNil(l.clientInit("sdk-test", 0, {})) end + function TestAll:testSetAllConfigFields() local c = l.clientInit("sdk-test", 0, { offline = true, @@ -64,8 +68,12 @@ function TestAll:testSetAllConfigFields() privateAttributes = {"/foo", "/bar"} }, logging = { - level = "debug", - tag = "LaunchDarklyLuaTest" + -- These two are mutually exclusive, but we're testing that both can be parsed. + custom = l.makeLogBackend(function(_) return true end, function(_, _) end), + basic = { + level = "debug", + tag = "LaunchDarklyLuaTest" + } } }) end From 3c5ac3679a1740fdfbe2636d7e850243c1caa684 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 27 Dec 2023 13:17:25 -0800 Subject: [PATCH 155/161] try removing boost depend from rockspec --- .github/actions/ci/action.yml | 6 ++---- launchdarkly-server-sdk-1.0-0.rockspec | 14 ++++---------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index ecd0256..05f02ed 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -30,15 +30,13 @@ runs: shell: bash run: | luarocks make launchdarkly-server-sdk-1.0-0.rockspec \ - LD_DIR=./cpp-sdk/build-dynamic/release \ - BOOST_DIR=${{ steps.install-boost.outputs.BOOST_ROOT }} + LD_DIR=./cpp-sdk/build-dynamic/release - name: Build Lua Server-side SDK with Redis shell: bash run: | luarocks make launchdarkly-server-sdk-redis-1.0-0.rockspec \ - LDREDIS_DIR=./cpp-sdk/build-dynamic/release \ - BOOST_DIR=${{ steps.install-boost.outputs.BOOST_ROOT }} + LDREDIS_DIR=./cpp-sdk/build-dynamic/release - name: Run Lua Server-side SDK Tests shell: bash diff --git a/launchdarkly-server-sdk-1.0-0.rockspec b/launchdarkly-server-sdk-1.0-0.rockspec index fe908e1..86b6e2c 100644 --- a/launchdarkly-server-sdk-1.0-0.rockspec +++ b/launchdarkly-server-sdk-1.0-0.rockspec @@ -18,15 +18,6 @@ external_dependencies = { header = "launchdarkly/server_side/bindings/c/sdk.h", library = "launchdarkly-cpp-server" }, - platforms = { - linux = { - BOOST = { - -- The library depends on boost_json and boost_url, but we only need to test for the existence - -- of one of them since they are both part of the boost distribution. - library = "libboost_json-mt-x64.so.1.81.0" - } - } - } } test = { @@ -39,9 +30,12 @@ build = { modules = { ["launchdarkly_server_sdk"] = { sources = { "launchdarkly-server-sdk.c" }, + -- Uncomment to compile with debug messages, mainly to help debug parsing configuration/context + -- builders. + -- defines = {"DEBUG=1"}, incdirs = {"$(LD_INCDIR)"}, libdirs = {"$(LD_LIBDIR)"}, - libraries = {"launchdarkly-cpp-server"} + libraries = {"launchdarkly-cpp-server"}, } } } From 501b98b2645dd36456c0cd3000cb58c9df7dd8cc Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 27 Dec 2023 13:47:29 -0800 Subject: [PATCH 156/161] update docs --- README.md | 44 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3d3f4e4..6f1a15c 100644 --- a/README.md +++ b/README.md @@ -14,16 +14,33 @@ LaunchDarkly overview Supported Lua versions ----------- -This version of the LaunchDarkly SDK is compatible with the Lua 5.1-5.3 interpreter, and LuaJIT. +This version of the LaunchDarkly SDK is known to be compatible with the Lua 5.1-5.3 interpreter, and LuaJIT 2.0.5. -Supported C server-side SDK versions +Supported C++ server-side SDK versions ----------- -This version of the Lua server-side SDK depends on the C++ server-side SDK C bindings, as well as the -C++ server-side SDK's Redis source integration. +This version of the Lua server-side SDK depends on the LaunchDarkly C++ Server-side SDK. -The minimum required version is `2.1.0` for the Redis source. +If Redis support is desired, then it optionally depends on the C++ server-side SDK's Redis Source. +| Dependency | Minimum Version | Notes | +|--------------------------------|------------------------------------------------------------------------------------------------------------|--------------------------------------------| +| C++ Server-Side SDK | [3.3.0](https://github.com/launchdarkly/cpp-sdks/releases/tag/launchdarkly-cpp-server-v3.3.0) | Required dependency. | +| C++ Server-Side SDK with Redis | [2.1.0](https://github.com/launchdarkly/cpp-sdks/releases/tag/launchdarkly-cpp-server-redis-source-v2.1.0) | Optional, if using Redis as a data source. | + + +3rd Party Dependencies +------------ +Depending on how the C++ server-side SDK was built, the Lua SDK may require additional runtime dependencies to work properly. + + +| Dependency | If C++ SDK compiled with.. | Notes | +|------------|------------------------------|------------------------------------------------------------------------| +| OpenSSL | `LD_DYNAMIC_LINK_OPENSSL=ON` | If linking OpenSSL dynamically, it must be present on target system. | +| Boost | `LD_DYNAMIC_LINK_BOOST=ON` | If linking Boost dynamically, it must be present on the target system. | + +_Note: The CI process builds against the C++ Server-side SDK's Linux shared libraries, which were compiled with `LD_DYNAMIC_LINK_BOOST=ON` so +Boost is fetched as part of the build process._ Getting started @@ -31,6 +48,23 @@ Getting started Refer to the [SDK documentation](https://docs.launchdarkly.com/sdk/server-side/lua#getting-started) for instructions on getting started with using the SDK. +To compile the LuaRock modules: +1. Install [LuaRocks](https://github.com/luarocks/luarocks/wiki/Download) +2. Build the [C++ Server-side SDK](https://github.com/launchdarkly/cpp-sdks) from source using CMake, or obtain pre-built artifacts from the [releases page](https://github.com/launchdarkly/cpp-sdks/releases?q=%22launchdarkly-cpp-server%22) +3. Run `luarocks make`: +```bash +# Base SDK +luarocks make launchdarkly-server-sdk-1.0-0.rockspec \ +LD_DIR=./path-to-installed-cpp-sdk + +# SDK with Redis +luarocks make launchdarkly-server-sdk-redis-1.0-0.rockspec \ +LDREDIS_DIR=./path-to-installed-cpp-sdk +``` + +Please note that the Lua SDK uses the C++ server-side SDK's C bindings, so if you're using prebuilt artifacts +then only a C99 compiler is necessary. + Learn more ----------- From a80171b2562cb38943bab5e63ecadfe8633f8c24 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 28 Dec 2023 11:11:53 -0800 Subject: [PATCH 157/161] Update scripts/get-cpp-sdk-locally.sh Co-authored-by: Matthew M. Keeler --- scripts/get-cpp-sdk-locally.sh | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/scripts/get-cpp-sdk-locally.sh b/scripts/get-cpp-sdk-locally.sh index 45f054f..bbc915a 100755 --- a/scripts/get-cpp-sdk-locally.sh +++ b/scripts/get-cpp-sdk-locally.sh @@ -13,15 +13,7 @@ set -e -branch="main" - -if [ -n "$1" ] -then - branch="$1" -fi - - -git clone --depth 1 --branch "$branch" https://github.com/launchdarkly/cpp-sdks.git +git clone --depth 1 --branch "${1:-main}" https://github.com/launchdarkly/cpp-sdks.git cd cpp-sdks mkdir build cd build From da43dd486e626e755780e1cf2aea35127c6e35eb Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 28 Dec 2023 11:01:38 -0800 Subject: [PATCH 158/161] remove type-specific checkers and replace with one generic routine --- launchdarkly-server-sdk.c | 47 +++++++++------------------------------ 1 file changed, 10 insertions(+), 37 deletions(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index f73bb10..812f1e3 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -432,9 +432,7 @@ LuaLDUserNew(lua_State *const l) static void parse_private_attrs_or_cleanup(lua_State *const l, LDContextBuilder builder, const char* kind); static void parse_attrs_or_cleanup(lua_State *const l, LDContextBuilder builder, const char* kind); -static bool field_is_table_or_cleanup(lua_State* const l, int field_type, LDContextBuilder builder, const char* field_name, const char* kind); -static bool field_is_string_or_cleanup(lua_State* const l, int field_type, LDContextBuilder builder, const char* field_name, const char* kind); -static bool field_is_bool_or_cleanup(lua_State* const l, int field_type, LDContextBuilder builder, const char* field_name, const char* kind); +static bool field_is_type_or_cleanup(lua_State* const l, int actual_field_type, int expected_field_type, LDContextBuilder builder, const char* field_name, const char* kind); /** @@ -546,26 +544,26 @@ LuaLDContextNew(lua_State *const l) { LDContextBuilder_AddKind(builder, kind, key); lua_getfield(l, -2, "attributes"); - if (field_is_table_or_cleanup(l, lua_type(l, -1), builder, "attributes", kind)) { + if (field_is_type_or_cleanup(l, lua_type(l, -1), LUA_TTABLE, builder, "attributes", kind)) { parse_attrs_or_cleanup(l, builder, kind); } lua_pop(l, 1); lua_getfield(l, -2, "privateAttributes"); - if (field_is_table_or_cleanup(l, lua_type(l, -1), builder, "privateAttributes", kind)) { + if (field_is_type_or_cleanup(l, lua_type(l, -1), LUA_TTABLE, builder, "privateAttributes", kind)) { parse_private_attrs_or_cleanup(l, builder, kind); } lua_pop(l, 1); lua_getfield(l, -2, "name"); - if (field_is_string_or_cleanup(l, lua_type(l, -1), builder, "name", kind)) { + if (field_is_type_or_cleanup(l, lua_type(l, -1), LUA_TSTRING, builder, "name", kind)) { LDContextBuilder_Attributes_SetName(builder, kind, lua_tostring(l, -1)); } lua_pop(l, 1); lua_getfield(l, -2, "anonymous"); - if (field_is_bool_or_cleanup(l, lua_type(l, -1), builder, "anonymous", kind)) { + if (field_is_type_or_cleanup(l, lua_type(l, -1), LUA_TBOOLEAN, builder, "anonymous", kind)) { LDContextBuilder_Attributes_SetAnonymous(builder, kind, lua_toboolean(l, -1)); } lua_pop(l, 1); @@ -588,41 +586,16 @@ LuaLDContextNew(lua_State *const l) { return 1; } -static bool field_is_table_or_cleanup(lua_State* const l, int field_type, LDContextBuilder builder, const char* field_name, const char* kind) { - if (field_type == LUA_TTABLE) { - DEBUG_PRINT("field %s for %s context is a table\n", field_name, kind); - return true; - } else if (field_type == LUA_TNIL) { - DEBUG_PRINT("no %s for %s context\n", field_name, kind); - } else { - LDContextBuilder_Free(builder); - luaL_error(l, "context kind %s: %s must be a table", kind, field_name); - } - return false; -} - -static bool field_is_bool_or_cleanup(lua_State* const l, int field_type, LDContextBuilder builder, const char* field_name, const char* kind) { - if (field_type == LUA_TBOOLEAN) { - DEBUG_PRINT("field %s for %s context is a boolean\n", field_name, kind); - return true; - } else if (field_type == LUA_TNIL) { - DEBUG_PRINT("no %s for %s context\n", field_name, kind); - } else { - LDContextBuilder_Free(builder); - luaL_error(l, "context kind %s: %s must be a boolean", kind, field_name); - } - return false; -} -static bool field_is_string_or_cleanup(lua_State* const l, int field_type, LDContextBuilder builder, const char* field_name, const char* kind) { - if (field_type == LUA_TSTRING) { - DEBUG_PRINT("field %s for %s context is a string\n", field_name, kind); +static bool field_is_type_or_cleanup(lua_State* const l, int actual_field_type, int expected_field_type, LDContextBuilder builder, const char* field_name, const char* kind) { + if (actual_field_type == expected_field_type) { + DEBUG_PRINT("field %s for %s context is a %s\n", field_name, kind, lua_typename(l, actual_field_type)); return true; - } else if (field_type == LUA_TNIL) { + } else if (actual_field_type == LUA_TNIL) { DEBUG_PRINT("no %s for %s context\n", field_name, kind); } else { LDContextBuilder_Free(builder); - luaL_error(l, "context kind %s: %s must be a string", kind, field_name); + luaL_error(l, "context kind %s: %s must be a %s", kind, field_name, lua_typename(l, expected_field_type)); } return false; } From f0eb8bed37be071efe16a3118366c502ae4a1da2 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 28 Dec 2023 11:15:55 -0800 Subject: [PATCH 159/161] indent markdown --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 6f1a15c..ba9c5ec 100644 --- a/README.md +++ b/README.md @@ -52,15 +52,15 @@ To compile the LuaRock modules: 1. Install [LuaRocks](https://github.com/luarocks/luarocks/wiki/Download) 2. Build the [C++ Server-side SDK](https://github.com/launchdarkly/cpp-sdks) from source using CMake, or obtain pre-built artifacts from the [releases page](https://github.com/launchdarkly/cpp-sdks/releases?q=%22launchdarkly-cpp-server%22) 3. Run `luarocks make`: -```bash -# Base SDK -luarocks make launchdarkly-server-sdk-1.0-0.rockspec \ -LD_DIR=./path-to-installed-cpp-sdk - -# SDK with Redis -luarocks make launchdarkly-server-sdk-redis-1.0-0.rockspec \ -LDREDIS_DIR=./path-to-installed-cpp-sdk -``` + ```bash + # Base SDK + luarocks make launchdarkly-server-sdk-1.0-0.rockspec \ + LD_DIR=./path-to-installed-cpp-sdk + + # SDK with Redis + luarocks make launchdarkly-server-sdk-redis-1.0-0.rockspec \ + LDREDIS_DIR=./path-to-installed-cpp-sdk + ``` Please note that the Lua SDK uses the C++ server-side SDK's C bindings, so if you're using prebuilt artifacts then only a C99 compiler is necessary. From e94b610d5c6104b144eca0012bda5a008664e4ff Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 28 Dec 2023 11:19:07 -0800 Subject: [PATCH 160/161] fix docs --- launchdarkly-server-sdk.c | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 812f1e3..9b212f8 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -445,8 +445,8 @@ For example, to create a context with a single user kind: local context = ld.makeContext({ user = { key = "alice-123", + name = "alice", attributes = { - name = "alice", age = 52, contact = { email = "alice@mail.com", @@ -465,9 +465,8 @@ For example, to associate a device context with a user: local context = ld.makeContext({ user = { key = "alice-123", - attributes = { - name = "alice" - } + name = "alice", + anonymous = true }, device { key = "device-123", @@ -485,13 +484,13 @@ by calling @{valid} to detect errors earlier. @tparam table A table of context kinds, where the table keys are the kind names and the values are tables containing context's information. @tparam string [kind.key] The context's key, which is required. +@tparam[opt] [kind.name] A name for the context. This is useful for identifying the context +in the LaunchDarkly dashboard. +@tparam[opt] [kind.anonymous] A boolean indicating whether the context should be marked as anonymous. @tparam[opt] [kind.attributes] A table of arbitrary attributes to associate with the context. @tparam[opt] [kind.privateAttributes] An array of attribute references, indicating which attributes should be marked private. Attribute references may be simple attribute names (like 'age'), or may use a JSON-pointer-like syntax (like '/contact/phone'). -@tparam[opt] [kind.name] A name for the context. This is useful for identifying the context -in the LaunchDarkly dashboard. -@tparam[opt] [kind.anonymous] A boolean indicating whether the context should be marked as anonymous. @treturn A fresh context. */ static int @@ -854,7 +853,7 @@ struct config { struct config name = {path, fields, ARR_SIZE(fields), NULL, NULL, NULL} // Use this macro to define a config table which requires a child builder. -#define DEFINE_SUB_CONFIG(name, path, fields, new_builder, consume_builder) \ +#define DEFINE_CHILD_CONFIG(name, path, fields, new_builder, consume_builder) \ struct config name = {path, fields, ARR_SIZE(fields), NULL, (new_child_builder_fn) new_builder, (consume_child_builder_fn) consume_builder} // Invokes a field's parse method, varying the builder argument depending on if this @@ -875,7 +874,7 @@ struct field_validator lazyload_fields[] = { FIELD("cacheEvictionPolicy", LUA_TNUMBER, parse_unsigned, LDServerLazyLoadBuilder_CachePolicy) }; -DEFINE_SUB_CONFIG(lazyload_config, +DEFINE_CHILD_CONFIG(lazyload_config, "dataSystem.lazyLoad", lazyload_fields, LDServerLazyLoadBuilder_New, @@ -886,7 +885,7 @@ struct field_validator streaming_fields[] = { FIELD("initialReconnectDelayMilliseconds", LUA_TNUMBER, parse_unsigned, LDServerDataSourceStreamBuilder_InitialReconnectDelayMs) }; -DEFINE_SUB_CONFIG(streaming_config, +DEFINE_CHILD_CONFIG(streaming_config, "dataSystem.backgroundSync.streaming", streaming_fields, LDServerDataSourceStreamBuilder_New, @@ -897,7 +896,7 @@ struct field_validator polling_fields[] = { FIELD("intervalSeconds", LUA_TNUMBER, parse_unsigned, LDServerDataSourcePollBuilder_IntervalS) }; -DEFINE_SUB_CONFIG(polling_config, +DEFINE_CHILD_CONFIG(polling_config, "dataSystem.backgroundSync.polling", polling_fields, LDServerDataSourcePollBuilder_New, @@ -952,7 +951,7 @@ struct field_validator basic_logging_fields[] = { FIELD("tag", LUA_TSTRING, parse_string, LDLoggingBasicBuilder_Tag) }; -DEFINE_SUB_CONFIG( +DEFINE_CHILD_CONFIG( basic_logging_config, "logging.basic", basic_logging_fields, @@ -1040,7 +1039,7 @@ makeConfig(lua_State *const l, const char *const sdk_key) { LDServerConfigBuilder builder = LDServerConfigBuilder_New(sdk_key); - // Recursively visit the heirarchical configs, modifying the builder + // Recursively visit the hierarchical configs, modifying the builder // as we go along. traverse_config(l, builder, &top_level_config); From 99c839174169d7327cdc9adb2750a3da0b486c78 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 28 Dec 2023 11:22:08 -0800 Subject: [PATCH 161/161] remove some todos --- launchdarkly-server-sdk.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c index 9b212f8..f6914c9 100644 --- a/launchdarkly-server-sdk.c +++ b/launchdarkly-server-sdk.c @@ -1243,8 +1243,6 @@ LuaPushDetails(lua_State *const l, LDEvalDetail details, LDValue value) { lua_newtable(l); - /** TODO: C bindings for this? */ - LDEvalReason out_reason; if (LDEvalDetail_Reason(details, &out_reason)) { @@ -1813,8 +1811,6 @@ LuaLDClientTrack(lua_State *const l) return 0; } -// TODO: Document alias was removed, use multi context instead - /*** Check if a client has been fully initialized. This may be useful if the initialization timeout was reached.