Skip to content

Commit f00a718

Browse files
authored
feat: Add experimental support for plugins (#140)
1 parent eef2779 commit f00a718

File tree

13 files changed

+721
-0
lines changed

13 files changed

+721
-0
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace LaunchDarkly.Sdk
2+
{
3+
/// <summary>
4+
/// The type of credential used for the environment.
5+
/// </summary>
6+
public enum CredentialType
7+
{
8+
/// <summary>
9+
/// A mobile key credential.
10+
/// </summary>
11+
MobileKey,
12+
/// <summary>
13+
/// An SDK key credential.
14+
/// </summary>
15+
SdkKey
16+
}
17+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
namespace LaunchDarkly.Sdk.Integrations.Plugins
2+
{
3+
/// <summary>
4+
/// Metadata about the application where the SDK is running.
5+
/// </summary>
6+
public sealed class ApplicationMetadata
7+
{
8+
/// <summary>
9+
/// Gets the application identifier.
10+
/// </summary>
11+
public string Id { get; }
12+
13+
/// <summary>
14+
/// Gets the application version.
15+
/// </summary>
16+
public string Version { get; }
17+
18+
/// <summary>
19+
/// Gets the application name.
20+
/// </summary>
21+
public string Name { get; }
22+
23+
/// <summary>
24+
/// Gets the application version name.
25+
/// </summary>
26+
public string VersionName { get; }
27+
28+
/// <summary>
29+
/// Initializes a new instance of the <see cref="ApplicationMetadata"/> class with the specified application ID, version, name, and version name.
30+
/// </summary>
31+
/// <param name="id">The application identifier.</param>
32+
/// <param name="version">The application version.</param>
33+
/// <param name="name">The application name.</param>
34+
/// <param name="versionName">The application version name.</param>
35+
public ApplicationMetadata(string id = null, string version = null, string name = null, string versionName = null)
36+
{
37+
Id = id;
38+
Version = version;
39+
Name = name;
40+
VersionName = versionName;
41+
}
42+
}
43+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
namespace LaunchDarkly.Sdk.Integrations.Plugins
2+
{
3+
/// <summary>
4+
/// Metadata about the environment where the SDK is running.
5+
/// </summary>
6+
public sealed class EnvironmentMetadata
7+
{
8+
/// <summary>
9+
/// Gets the SDK metadata.
10+
/// </summary>
11+
public SdkMetadata Sdk { get; }
12+
13+
/// <summary>
14+
/// Gets the SDK key.
15+
/// </summary>
16+
public string Credential { get; }
17+
18+
/// <summary>
19+
/// Gets the type of credential used (e.g., Mobile Key or SDK Key).
20+
/// </summary>
21+
public CredentialType CredentialType { get; }
22+
23+
/// <summary>
24+
/// Gets the application metadata.
25+
/// </summary>
26+
public ApplicationMetadata Application { get; }
27+
28+
/// <summary>
29+
/// Initializes a new instance of the <see cref="EnvironmentMetadata"/> class.
30+
/// </summary>
31+
/// <param name="sdkMetadata">the SDK metadata</param>
32+
/// <param name="credential">the SDK Key or Mobile Key</param>
33+
/// <param name="credentialType">the type of credential</param>
34+
/// <param name="applicationMetadata">the application metadata</param>
35+
public EnvironmentMetadata(SdkMetadata sdkMetadata, string credential, CredentialType credentialType, ApplicationMetadata applicationMetadata)
36+
{
37+
Sdk = sdkMetadata;
38+
Credential = credential;
39+
CredentialType = credentialType;
40+
Application = applicationMetadata;
41+
}
42+
}
43+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using System.Collections.Generic;
2+
3+
namespace LaunchDarkly.Sdk.Integrations.Plugins
4+
{
5+
/// <summary>
6+
/// Abstract base class for extending SDK functionality via plugins.
7+
/// Consumers should provide specific implementations of this class with the appropariate
8+
/// client and hook types for their use case.
9+
/// This class includes default implementations for optional methods, allowing
10+
/// LaunchDarkly to expand the list of plugin methods without breaking customer integrations.
11+
/// Plugins provide an interface which allows for initialization, access to credentials,
12+
/// and hook registration in a single interface.
13+
/// </summary>
14+
/// <typeparam name="TClient">The type of the LaunchDarkly client (e.g., ILdClient)</typeparam>
15+
/// <typeparam name="THook">The type of hooks used by this plugin (e.g., Hook)</typeparam>
16+
public abstract class PluginBase<TClient, THook>
17+
{
18+
/// <summary>
19+
/// Get metadata about the plugin implementation.
20+
/// </summary>
21+
/// <returns>Metadata describing this plugin</returns>
22+
public PluginMetadata Metadata { get; private set; }
23+
24+
/// <summary>
25+
/// Initializes a new instance of the <see cref="PluginBase{TClient, THook}"/> class with the specified name.
26+
/// </summary>
27+
/// <param name="name">The name of the plugin.</param>
28+
public PluginBase(string name)
29+
{
30+
Metadata = new PluginMetadata(name);
31+
}
32+
33+
/// <summary>
34+
/// Registers the plugin with the specified LaunchDarkly client and environment metadata.
35+
/// </summary>
36+
/// <param name="client">An instance of the LaunchDarkly client to register the plugin with.</param>
37+
/// <param name="metadata">Metadata about the environment.</param>
38+
public abstract void Register(TClient client, EnvironmentMetadata metadata);
39+
40+
/// <summary>
41+
/// Returns a list of hooks to be registered for the plugin, based on the provided environment metadata.
42+
/// </summary>
43+
/// <param name="metadata">Metadata about the environment.</param>
44+
/// <returns>A list of hook instances to be registered.</returns>
45+
public virtual IList<THook> GetHooks(EnvironmentMetadata metadata)
46+
{
47+
return new List<THook>();
48+
}
49+
}
50+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using LaunchDarkly.Logging;
5+
6+
namespace LaunchDarkly.Sdk.Integrations.Plugins
7+
{
8+
/// <summary>
9+
/// Extension methods for plugin registration and hook collection.
10+
/// </summary>
11+
public static class PluginExtensions
12+
{
13+
/// <summary>
14+
/// Registers all plugins with the client and environment metadata.
15+
/// </summary>
16+
/// <typeparam name="TClient">The client type (e.g., ILdClient)</typeparam>
17+
/// <typeparam name="THook">The hook type (e.g., Hook)</typeparam>
18+
/// <param name="client">The client instance to register plugins with</param>
19+
/// <param name="plugins">The collection of plugins to register</param>
20+
/// <param name="environmentMetadata">Metadata about the environment</param>
21+
/// <param name="logger">Logger for error reporting</param>
22+
/// <remarks>
23+
/// This method iterates through each plugin in the collection and calls its `Register` method
24+
/// to initialize it with the client and environment metadata. It logs any exceptions that occur during
25+
/// the registration process, allowing the client to continue functioning even if some plugins fail to register.
26+
/// </remarks>
27+
public static void RegisterPlugins<TClient, THook>(
28+
this TClient client,
29+
IEnumerable<PluginBase<TClient, THook>> plugins,
30+
EnvironmentMetadata environmentMetadata,
31+
Logger logger)
32+
{
33+
foreach (var plugin in plugins)
34+
{
35+
try
36+
{
37+
plugin.Register(client, environmentMetadata);
38+
}
39+
catch (Exception ex)
40+
{
41+
logger.Error("Error registering plugin {0}: {1}",
42+
plugin.Metadata.Name ?? "unknown", ex);
43+
}
44+
}
45+
}
46+
47+
/// <summary>
48+
/// Retrieves all hooks from the specified plugins.
49+
/// </summary>
50+
/// <typeparam name="TClient">The client type</typeparam>
51+
/// <typeparam name="THook">The hook type</typeparam>
52+
/// <param name="client">The client instance to register plugins with</param>
53+
/// <param name="plugins">The collection of plugins</param>
54+
/// <param name="environmentMetadata">Metadata about the environment</param>
55+
/// <param name="logger">Logger for error reporting</param>
56+
/// <returns>A list of hooks from all plugins</returns>
57+
/// <remarks>
58+
/// This method iterates through each plugin in the collection and calls its `GetHooks` method
59+
/// to retrieve any hooks the plugin provides. It logs any exceptions that occur during
60+
/// the hook retrieval process and continues processing remaining plugins.
61+
/// </remarks>
62+
public static List<THook> GetPluginHooks<TClient, THook>(
63+
this TClient client,
64+
IEnumerable<PluginBase<TClient, THook>> plugins,
65+
EnvironmentMetadata environmentMetadata,
66+
Logger logger)
67+
{
68+
var allHooks = new List<THook>();
69+
foreach (var plugin in plugins)
70+
{
71+
try
72+
{
73+
var pluginHooks = plugin.GetHooks(environmentMetadata);
74+
if (pluginHooks != null)
75+
{
76+
allHooks.AddRange(pluginHooks);
77+
}
78+
}
79+
catch (Exception ex)
80+
{
81+
logger.Error("Error getting hooks from plugin {0}: {1}",
82+
plugin.Metadata.Name ?? "unknown", ex);
83+
}
84+
}
85+
return allHooks;
86+
}
87+
}
88+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System;
2+
3+
namespace LaunchDarkly.Sdk.Integrations.Plugins
4+
{
5+
/// <summary>
6+
/// Metadata about a plugin implementation.
7+
/// </summary>
8+
public sealed class PluginMetadata
9+
{
10+
/// <summary>
11+
/// The name of the plugin.
12+
/// </summary>
13+
public string Name { get; }
14+
15+
/// <summary>
16+
/// Initializes a new instance of the <see cref="PluginMetadata"/> class with the specified name.
17+
/// </summary>
18+
/// <param name="name">The name of the plugin.</param>
19+
public PluginMetadata(string name)
20+
{
21+
Name = name;
22+
}
23+
}
24+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
namespace LaunchDarkly.Sdk.Integrations.Plugins
2+
{
3+
/// <summary>
4+
/// Metadata about the SDK itself.
5+
/// </summary>
6+
public sealed class SdkMetadata
7+
{
8+
/// <summary>
9+
/// Gets the id of the SDK. This should match the identifier in the SDK.
10+
/// This field should be either the x-launchdarkly-user-agent or the user-agent.
11+
/// </summary>
12+
public string Name { get; }
13+
14+
/// <summary>
15+
/// Gets the version of the SDK.
16+
/// </summary>
17+
public string Version { get; }
18+
19+
/// <summary>
20+
/// Gets the wrapper name if this SDK is a wrapper.
21+
/// </summary>
22+
public string WrapperName { get; }
23+
24+
/// <summary>
25+
/// Gets the wrapper version if this SDK is a wrapper.
26+
/// </summary>
27+
public string WrapperVersion { get; }
28+
29+
/// <summary>
30+
/// Initializes a new instance of the <see cref="SdkMetadata"/> class.
31+
/// </summary>
32+
/// <param name="name">The id of the SDK. This should match the identifier in the SDK. It should be either the x-launchdarkly-user-agent or the user-agent.</param>
33+
/// <param name="version">The version of the SDK.</param>
34+
/// <param name="wrapperName">If this SDK is a wrapper, then this should be the wrapper name.</param>
35+
/// <param name="wrapperVersion">If this SDK is a wrapper, then this should be the wrapper version.</param>
36+
public SdkMetadata(string name, string version, string wrapperName = null, string wrapperVersion = null)
37+
{
38+
Name = name;
39+
Version = version;
40+
WrapperName = wrapperName;
41+
WrapperVersion = wrapperVersion;
42+
}
43+
}
44+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using Xunit;
2+
3+
namespace LaunchDarkly.Sdk.Integrations.Plugins
4+
{
5+
public class ApplicationMetadataTest
6+
{
7+
[Fact]
8+
public void CanConstructWithAllParameters()
9+
{
10+
var appMetadata = new ApplicationMetadata("app-id", "1.0.0", "My App", "v1.0.0");
11+
12+
Assert.Equal("app-id", appMetadata.Id);
13+
Assert.Equal("1.0.0", appMetadata.Version);
14+
Assert.Equal("My App", appMetadata.Name);
15+
Assert.Equal("v1.0.0", appMetadata.VersionName);
16+
}
17+
18+
[Fact]
19+
public void CanConstructWithDefaultParameters()
20+
{
21+
var appMetadata = new ApplicationMetadata();
22+
23+
Assert.Null(appMetadata.Id);
24+
Assert.Null(appMetadata.Version);
25+
Assert.Null(appMetadata.Name);
26+
Assert.Null(appMetadata.VersionName);
27+
}
28+
}
29+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using Xunit;
2+
3+
namespace LaunchDarkly.Sdk.Integrations.Plugins
4+
{
5+
public class EnvironmentMetadataTest
6+
{
7+
[Fact]
8+
public void CanConstructWithAllParameters()
9+
{
10+
var sdkMetadata = new SdkMetadata("dotnet-server-sdk", "6.0.0");
11+
var appMetadata = new ApplicationMetadata("app-id", "1.0.0");
12+
var envMetadata = new EnvironmentMetadata(sdkMetadata, "test-sdk-key", CredentialType.SdkKey, appMetadata);
13+
14+
Assert.Equal(sdkMetadata, envMetadata.Sdk);
15+
Assert.Equal("test-sdk-key", envMetadata.Credential);
16+
Assert.Equal(CredentialType.SdkKey, envMetadata.CredentialType);
17+
Assert.Equal(appMetadata, envMetadata.Application);
18+
}
19+
}
20+
}

0 commit comments

Comments
 (0)