Initial commit

This commit is contained in:
rudder
2026-08-12 14:04:55 +03:00
commit 8cf738d9f0
124 changed files with 5433 additions and 0 deletions
+155
View File
@@ -0,0 +1,155 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using RudderSdk.Core.Models.RemoteConfig;
namespace RudderSdk.Core;
/// <summary>
/// Remote configuration values. <see cref="LoadAsync"/> caches every config
/// whose "active" flag is not explicitly false; <see cref="Get{T}"/> reads
/// typed values from the cache.
/// </summary>
public sealed class RemoteConfigService
{
private readonly RudderClient _client;
private readonly Dictionary<string, RemoteConfig> _cache = new();
private bool _isLoaded;
internal RemoteConfigService(RudderClient client) => _client = client;
/// <summary>True after the first successful <see cref="LoadAsync"/>.</summary>
public bool IsLoaded => _isLoaded;
/// <summary>The cached configs keyed by config key.</summary>
public IReadOnlyDictionary<string, RemoteConfig> Configs => _cache;
/// <summary>Fetches all configs and rebuilds the cache.</summary>
public async Task<IReadOnlyDictionary<string, RemoteConfig>> LoadAsync(CancellationToken cancellationToken = default)
{
var response = await _client.SendAsync<RawListRemoteConfigsResponse>(
"GET",
"/sdk/v1/remote-configs",
cancellationToken).ConfigureAwait(false);
_cache.Clear();
if (response?.Configs != null)
{
foreach (var entry in response.Configs)
{
// Include everything except an explicit "active": false — a missing
// flag means active, matching the other SDKs.
if (entry.Value?["active"]?.Value<bool?>() == false)
continue;
var config = entry.Value?.ToObject<RemoteConfig>();
if (config != null)
_cache[config.Key ?? entry.Key] = config;
}
}
_isLoaded = true;
return _cache;
}
/// <summary>Fetches one config straight from the server, bypassing the cache.</summary>
public async Task<RemoteConfig> GetConfigAsync(string key, CancellationToken cancellationToken = default)
{
return await _client.SendAsync<RemoteConfig>(
"GET",
"/sdk/v1/remote-configs/" + Url.Encode(key),
cancellationToken).ConfigureAwait(false);
}
/// <summary>Reads a typed value from the cache; returns the default when missing or unparsable.</summary>
public T Get<T>(string key, T defaultValue = default!)
{
if (!_isLoaded || string.IsNullOrEmpty(key) || !_cache.TryGetValue(key, out var config))
return defaultValue;
return ParseValue(config.Value, config.ValueType, defaultValue);
}
/// <summary>Loads the cache on first use, then reads a typed value.</summary>
public async Task<T> GetAsync<T>(string key, T defaultValue = default!, CancellationToken cancellationToken = default)
{
if (!_isLoaded)
await LoadAsync(cancellationToken).ConfigureAwait(false);
return Get(key, defaultValue);
}
internal void ApplyOverride(string key, string? value, string valueType = "json")
{
if (string.IsNullOrEmpty(key)) return;
_cache[key] = new RemoteConfig
{
Key = key,
// The generated model declares Value non-nullable; a JSON null patch
// value is still legal and ParseValue maps it to the caller's default.
Value = value!,
ValueType = valueType,
Active = true
};
_isLoaded = true;
}
private static T ParseValue<T>(string value, string valueType, T defaultValue)
{
if (value == null) return defaultValue;
try
{
if (typeof(T) == typeof(string))
return (T)(object)value;
switch ((valueType ?? string.Empty).ToLowerInvariant())
{
case "number":
case "int":
case "integer":
case "float":
case "double":
return (T)ConvertNumber(value, typeof(T));
case "bool":
case "boolean":
return (T)(object)bool.Parse(value);
case "json":
case "object":
return JsonConvert.DeserializeObject<T>(value) ?? defaultValue;
default:
return (T)System.Convert.ChangeType(value, typeof(T), CultureInfo.InvariantCulture);
}
}
catch
{
return defaultValue;
}
}
private static object ConvertNumber(string value, Type target)
{
if (target == typeof(float))
return float.Parse(value, CultureInfo.InvariantCulture);
if (target == typeof(double))
return double.Parse(value, CultureInfo.InvariantCulture);
if (target == typeof(long))
return long.Parse(value, CultureInfo.InvariantCulture);
if (target == typeof(decimal))
return decimal.Parse(value, CultureInfo.InvariantCulture);
return int.Parse(value, CultureInfo.InvariantCulture);
}
// Raw shape of the list payload: the generated RemoteConfig.Active is a
// non-nullable bool and cannot distinguish "active": false from a missing
// flag, so the active check runs on the raw JSON before mapping.
private sealed class RawListRemoteConfigsResponse
{
[JsonProperty("configs")]
public Dictionary<string, JObject>? Configs { get; set; }
}
}