498 lines
19 KiB
C#
498 lines
19 KiB
C#
using System.Collections.Generic;
|
||
using System.Globalization;
|
||
using System.Linq;
|
||
|
||
namespace Sozsoft.Platform.Editors;
|
||
|
||
/// <summary>
|
||
/// Tek bir Script Builder kurali. <see cref="EditorScript"/> uzerindeki tarif
|
||
/// metotlariyla olusturulur, akici metotlarla kosul ve tetikleyici eklenir.
|
||
/// </summary>
|
||
public sealed class EditorScriptRule
|
||
{
|
||
/// <summary>Zorunlu parametreler. Dialogdaki isRuleComplete kontrolunun karsiligi.</summary>
|
||
private static readonly Dictionary<string, string[]> RequiredParams = new Dictionary<string, string[]>
|
||
{
|
||
["multiply"] = new[] { "left", "right", "target" },
|
||
["subtract"] = new[] { "left", "right", "target" },
|
||
["percent"] = new[] { "amount", "rate", "target" },
|
||
["sum"] = new[] { "target" },
|
||
["formula"] = new[] { "target", "expression" },
|
||
["today"] = new[] { "target" },
|
||
["days"] = new[] { "start", "end", "target" },
|
||
["hours"] = new[] { "start", "end", "target" },
|
||
["copy"] = new[] { "source", "target" },
|
||
["setValue"] = new[] { "target", "text" },
|
||
["clear"] = new string[0],
|
||
["readOnly"] = new[] { "target" },
|
||
["notify"] = new[] { "message" },
|
||
["ask"] = new[] { "message" },
|
||
["openUrl"] = new[] { "url" },
|
||
["apiToField"] = new[] { "target", "url" },
|
||
["location"] = new[] { "lat", "lng" },
|
||
["custom"] = new[] { "code" }
|
||
};
|
||
|
||
/// <summary>Alan listesi isteyen tarifler.</summary>
|
||
private static readonly HashSet<string> FieldListRecipes = new HashSet<string> { "sum", "clear" };
|
||
|
||
/// <summary>
|
||
/// Grup bazli varsayilan tetikleyici. Deger ureten kurallar form acilirken de
|
||
/// calisir; kullaniciya bir sey gosteren ya da dis cagri yapan kurallar
|
||
/// yalnizca deger degistiginde calisir.
|
||
/// </summary>
|
||
private static readonly Dictionary<string, EditorScriptTrigger> GroupDefaultTrigger =
|
||
new Dictionary<string, EditorScriptTrigger>
|
||
{
|
||
["multiply"] = EditorScriptTrigger.Both,
|
||
["subtract"] = EditorScriptTrigger.Both,
|
||
["percent"] = EditorScriptTrigger.Both,
|
||
["sum"] = EditorScriptTrigger.Both,
|
||
["formula"] = EditorScriptTrigger.Both,
|
||
["today"] = EditorScriptTrigger.Both,
|
||
["days"] = EditorScriptTrigger.Both,
|
||
["hours"] = EditorScriptTrigger.Both,
|
||
["copy"] = EditorScriptTrigger.Both,
|
||
["setValue"] = EditorScriptTrigger.Both,
|
||
["clear"] = EditorScriptTrigger.Both,
|
||
["readOnly"] = EditorScriptTrigger.Both,
|
||
["notify"] = EditorScriptTrigger.Change,
|
||
["ask"] = EditorScriptTrigger.Change,
|
||
["openUrl"] = EditorScriptTrigger.Change,
|
||
["apiToField"] = EditorScriptTrigger.Change,
|
||
["location"] = EditorScriptTrigger.Change,
|
||
["custom"] = EditorScriptTrigger.Change
|
||
};
|
||
|
||
private readonly string _recipe;
|
||
private readonly List<KeyValuePair<string, string>> _params = new List<KeyValuePair<string, string>>();
|
||
private readonly List<string> _fields = new List<string>();
|
||
private readonly List<EditorScriptCondition> _conditions = new List<EditorScriptCondition>();
|
||
|
||
private string _id;
|
||
private bool _anyCondition;
|
||
private EditorScriptTrigger _trigger = EditorScriptTrigger.Default;
|
||
|
||
internal EditorScriptRule(string recipe)
|
||
{
|
||
_recipe = recipe;
|
||
}
|
||
|
||
/* ---------------------------------------------------------------- *
|
||
* Akici yapilandirma
|
||
* ---------------------------------------------------------------- */
|
||
|
||
/// <summary>Kural yalnizca verilen kosullarin <b>hepsi</b> saglandiginda calisir.</summary>
|
||
public EditorScriptRule When(params EditorScriptCondition[] conditions) => AddConditions(conditions, false);
|
||
|
||
/// <summary>Kural verilen kosullardan <b>herhangi biri</b> saglandiginda calisir.</summary>
|
||
public EditorScriptRule WhenAny(params EditorScriptCondition[] conditions) => AddConditions(conditions, true);
|
||
|
||
/// <summary>Yalnizca alanin degeri degistiginde calissin.</summary>
|
||
public EditorScriptRule OnChange() => WithTrigger(EditorScriptTrigger.Change);
|
||
|
||
/// <summary>Yalnizca form acilirken calissin (varsayilan deger uretmek icin).</summary>
|
||
public EditorScriptRule OnOpen() => WithTrigger(EditorScriptTrigger.Open);
|
||
|
||
/// <summary>Hem acilista hem deger degistiginde calissin.</summary>
|
||
public EditorScriptRule OnOpenAndChange() => WithTrigger(EditorScriptTrigger.Both);
|
||
|
||
/// <summary>Tetikleyiciyi acikca belirler. Default ise tarifin grup varsayilani kullanilir.</summary>
|
||
public EditorScriptRule WithTrigger(EditorScriptTrigger trigger)
|
||
{
|
||
_trigger = trigger;
|
||
return this;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Kural kimligini sabitler. Verilmezse Build sirasinda tarif adi ve sira
|
||
/// numarasindan uretilir (ornek: "multiply_1").
|
||
/// </summary>
|
||
public EditorScriptRule WithId(string id)
|
||
{
|
||
_id = id;
|
||
return this;
|
||
}
|
||
|
||
/// <summary>
|
||
/// <see cref="ToJson"/> ciktisini (dialogun <c>@builder</c> basligindaki kural) geri okur; bilinmeyen
|
||
/// tarif <see cref="IsComplete"/>'ten gecmez ve uretimde atlanir.
|
||
/// </summary>
|
||
internal static EditorScriptRule FromJson(System.Text.Json.JsonElement json)
|
||
{
|
||
static string Text(System.Text.Json.JsonElement element, string name) =>
|
||
element.ValueKind == System.Text.Json.JsonValueKind.Object && element.TryGetProperty(name, out var value)
|
||
? value.ValueKind == System.Text.Json.JsonValueKind.String ? value.GetString() : value.ToString()
|
||
: null;
|
||
|
||
static EditorScriptCondition Condition(System.Text.Json.JsonElement element) =>
|
||
EditorScriptCondition.Of(Text(element, "operator") ?? "always", Text(element, "source"), Text(element, "value"));
|
||
|
||
var rule = new EditorScriptRule(Text(json, "recipe") ?? string.Empty).WithId(Text(json, "id"));
|
||
|
||
if (json.TryGetProperty("params", out var parameters) && parameters.ValueKind == System.Text.Json.JsonValueKind.Object)
|
||
{
|
||
foreach (var parameter in parameters.EnumerateObject())
|
||
rule.Set(parameter.Name, Text(parameters, parameter.Name));
|
||
}
|
||
|
||
if (json.TryGetProperty("fields", out var fields) && fields.ValueKind == System.Text.Json.JsonValueKind.Array)
|
||
rule.SetFields(fields.EnumerateArray().Select(field => field.ToString()));
|
||
|
||
var conditions = new List<EditorScriptCondition>();
|
||
if (json.TryGetProperty("condition", out var condition))
|
||
conditions.Add(Condition(condition));
|
||
if (json.TryGetProperty("conditions", out var more) && more.ValueKind == System.Text.Json.JsonValueKind.Array)
|
||
conditions.AddRange(more.EnumerateArray().Select(Condition));
|
||
|
||
// Ek "always" kosullari dialogun basliginda durur; atilirsa ToJson farkli baslik uretir ve
|
||
// script dialogda "elle duzenlenmis" sayilir. Tek "always" ise EffectiveConditions ile aynidir.
|
||
rule.AddConditions([.. conditions], Text(json, "join") == "or");
|
||
|
||
return rule.WithTrigger(Text(json, "trigger") switch
|
||
{
|
||
"change" => EditorScriptTrigger.Change,
|
||
"open" => EditorScriptTrigger.Open,
|
||
"both" => EditorScriptTrigger.Both,
|
||
_ => EditorScriptTrigger.Default
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// Mantiksal alana yazan <c>setValue</c> kuralinin degerini <c>true</c>/<c>false</c>'a cevirir; dialog bu
|
||
/// alanlarda yalnizca Evet/Hayir sectirir. <c>tpl("0")</c> metni onay kutusunu dolu sayardi.
|
||
/// </summary>
|
||
internal EditorScriptRule WithBooleanValue(System.Func<string, bool> isBooleanField)
|
||
{
|
||
if (_recipe != "setValue" || !isBooleanField(Param("target")))
|
||
{
|
||
return this;
|
||
}
|
||
|
||
var flag = Param("text").Trim().ToLowerInvariant() switch
|
||
{
|
||
"true" or "1" or "yes" or "evet" or "on" => "true",
|
||
"false" or "0" or "no" or "hayir" or "hayır" or "off" => "false",
|
||
_ => null
|
||
};
|
||
|
||
var index = _params.FindIndex(pair => pair.Key == "text");
|
||
if (flag != null && index >= 0)
|
||
{
|
||
_params[index] = new KeyValuePair<string, string>("text", flag);
|
||
}
|
||
|
||
return this;
|
||
}
|
||
|
||
/// <summary>Tek kurallik script metni. Birden fazla kural icin <see cref="EditorScript.Build(EditorScriptRule[])"/> kullanin.</summary>
|
||
public string ToScript() => EditorScript.Build(this);
|
||
|
||
/// <summary>Kurali dogrudan EditorScript alanina atanabilir hale getirir.</summary>
|
||
public static implicit operator string(EditorScriptRule rule) => rule?.ToScript() ?? string.Empty;
|
||
|
||
/* ---------------------------------------------------------------- *
|
||
* Ic kullanim
|
||
* ---------------------------------------------------------------- */
|
||
|
||
internal EditorScriptRule Set(string key, string value)
|
||
{
|
||
// Bos parametreler dialogda da yazilmaz; JSON'un birebir esit kalmasi icin atlanir.
|
||
if (!string.IsNullOrEmpty(value))
|
||
{
|
||
_params.Add(new KeyValuePair<string, string>(key, value));
|
||
}
|
||
|
||
return this;
|
||
}
|
||
|
||
internal EditorScriptRule SetNumber(string key, int? value)
|
||
=> value.HasValue ? Set(key, value.Value.ToString(CultureInfo.InvariantCulture)) : this;
|
||
|
||
internal EditorScriptRule SetFields(IEnumerable<string> fields)
|
||
{
|
||
if (fields != null)
|
||
{
|
||
_fields.AddRange(fields.Where(field => !string.IsNullOrEmpty(field)));
|
||
}
|
||
|
||
return this;
|
||
}
|
||
|
||
internal void EnsureId(int order)
|
||
{
|
||
if (string.IsNullOrEmpty(_id))
|
||
{
|
||
_id = _recipe + "_" + order.ToString(CultureInfo.InvariantCulture);
|
||
}
|
||
}
|
||
|
||
internal EditorScriptTrigger EffectiveTrigger()
|
||
{
|
||
if (_trigger != EditorScriptTrigger.Default)
|
||
{
|
||
return _trigger;
|
||
}
|
||
|
||
return GroupDefaultTrigger.TryGetValue(_recipe, out var trigger) ? trigger : EditorScriptTrigger.Change;
|
||
}
|
||
|
||
internal bool IsComplete()
|
||
{
|
||
if (!RequiredParams.TryGetValue(_recipe, out var required))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (_conditions.Any(condition => !condition.IsComplete()))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (FieldListRecipes.Contains(_recipe) && _fields.Count == 0)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
return required.All(key => _params.Any(pair => pair.Key == key && pair.Value.Trim().Length > 0));
|
||
}
|
||
|
||
internal string ToJson()
|
||
{
|
||
var conditions = EffectiveConditions();
|
||
|
||
var json = "{\"id\":" + EditorScript.Quote(_id) +
|
||
",\"recipe\":" + EditorScript.Quote(_recipe) +
|
||
",\"condition\":" + conditions[0].ToJson();
|
||
|
||
if (conditions.Count > 1)
|
||
{
|
||
json += ",\"conditions\":[" + string.Join(",", conditions.Skip(1).Select(c => c.ToJson())) + "]" +
|
||
",\"join\":" + EditorScript.Quote(_anyCondition ? "or" : "and");
|
||
}
|
||
|
||
json += ",\"params\":{" +
|
||
string.Join(",", _params.Select(pair => EditorScript.Quote(pair.Key) + ":" + EditorScript.Quote(pair.Value))) +
|
||
"}";
|
||
|
||
json += ",\"fields\":[" + string.Join(",", _fields.Select(EditorScript.Quote)) + "]";
|
||
|
||
if (_trigger != EditorScriptTrigger.Default)
|
||
{
|
||
json += ",\"trigger\":" + EditorScript.Quote(TriggerName(_trigger));
|
||
}
|
||
|
||
return json + "}";
|
||
}
|
||
|
||
/// <summary>
|
||
/// Kuralin tek satirlik kodu. runsOnOpen yalnizca script'in tamami acilista da
|
||
/// calisiyorsa true gelir; o durumda her kuralin ne zaman calisacagi isReady
|
||
/// ile ayristirilir.
|
||
/// </summary>
|
||
internal string ToCode(bool runsOnOpen)
|
||
{
|
||
var body = BuildBody();
|
||
if (string.IsNullOrEmpty(body))
|
||
{
|
||
return string.Empty;
|
||
}
|
||
|
||
var guards = new List<string>();
|
||
|
||
if (runsOnOpen)
|
||
{
|
||
var trigger = EffectiveTrigger();
|
||
if (trigger == EditorScriptTrigger.Change)
|
||
{
|
||
guards.Add("!isReady");
|
||
}
|
||
else if (trigger == EditorScriptTrigger.Open)
|
||
{
|
||
guards.Add("isReady");
|
||
}
|
||
}
|
||
|
||
// readOnly'de kosul dogrudan aksiyonun argumanidir, if ile sarmalanmaz.
|
||
if (_recipe != "readOnly")
|
||
{
|
||
var condition = ConditionExpression();
|
||
if (!string.IsNullOrEmpty(condition))
|
||
{
|
||
guards.Add(condition);
|
||
}
|
||
}
|
||
|
||
return guards.Count > 0 ? "if (" + string.Join(" && ", guards) + ") " + body : body;
|
||
}
|
||
|
||
private List<EditorScriptCondition> EffectiveConditions()
|
||
=> _conditions.Count > 0 ? _conditions : new List<EditorScriptCondition> { EditorScriptCondition.Always() };
|
||
|
||
private string ConditionExpression()
|
||
{
|
||
var parts = EffectiveConditions()
|
||
.Select(condition => condition.ToExpression())
|
||
.Where(expression => !string.IsNullOrEmpty(expression))
|
||
.ToList();
|
||
|
||
if (parts.Count == 0)
|
||
{
|
||
return string.Empty;
|
||
}
|
||
|
||
if (parts.Count == 1)
|
||
{
|
||
return parts[0];
|
||
}
|
||
|
||
return string.Join(_anyCondition ? " || " : " && ", parts.Select(part => "(" + part + ")"));
|
||
}
|
||
|
||
private EditorScriptRule AddConditions(EditorScriptCondition[] conditions, bool any)
|
||
{
|
||
if (conditions != null)
|
||
{
|
||
_conditions.AddRange(conditions.Where(condition => condition != null));
|
||
}
|
||
|
||
if (_conditions.Count > 1)
|
||
{
|
||
_anyCondition = any;
|
||
}
|
||
|
||
return this;
|
||
}
|
||
|
||
private string Param(string key)
|
||
{
|
||
var match = _params.FirstOrDefault(pair => pair.Key == key);
|
||
return match.Key == null ? string.Empty : match.Value;
|
||
}
|
||
|
||
private string Digits() => Param("digits").Length > 0 ? Param("digits") : "2";
|
||
|
||
private string BuildBody()
|
||
{
|
||
var q = new System.Func<string, string>(EditorScript.Quote);
|
||
var n = new System.Func<string, string>(EditorScript.NumberOrField);
|
||
|
||
switch (_recipe)
|
||
{
|
||
case "multiply":
|
||
return "set(" + q(Param("target")) + ", round(" + n(Param("left")) + " * " + n(Param("right")) + ", " + Digits() + "))";
|
||
|
||
case "subtract":
|
||
return "set(" + q(Param("target")) + ", round(" + n(Param("left")) + " - " + n(Param("right")) + ", " + Digits() + "))";
|
||
|
||
case "percent":
|
||
{
|
||
var amount = n(Param("amount"));
|
||
var portion = amount + " * " + n(Param("rate")) + " / 100";
|
||
var expression = Param("mode") == "add"
|
||
? amount + " + " + portion
|
||
: Param("mode") == "subtract"
|
||
? amount + " - " + portion
|
||
: portion;
|
||
return "set(" + q(Param("target")) + ", round(" + expression + ", " + Digits() + "))";
|
||
}
|
||
|
||
case "sum":
|
||
return "set(" + q(Param("target")) + ", round(sum(" + string.Join(", ", _fields.Select(q)) + "), " + Digits() + "))";
|
||
|
||
case "formula":
|
||
return "set(" + q(Param("target")) + ", " + (Param("expression").Length > 0 ? Param("expression") : "null") + ")";
|
||
|
||
case "today":
|
||
{
|
||
// TS ile ayni: gun sayisi tam sayiya kirpilir (Math.trunc(Number(offset))).
|
||
var offset = Param("offset");
|
||
var parsed = decimal.TryParse(offset, NumberStyles.Float, CultureInfo.InvariantCulture, out var value)
|
||
? (int)decimal.Truncate(value)
|
||
: 0;
|
||
var start = parsed != 0
|
||
? "new Date(Date.now() + " + parsed.ToString(CultureInfo.InvariantCulture) + " * 86400000)"
|
||
: "new Date()";
|
||
return "set(" + q(Param("target")) + ", " + start + ".toISOString().slice(0, 10))";
|
||
}
|
||
|
||
case "days":
|
||
return "set(" + q(Param("target")) + ", days(" + q(Param("start")) + ", " + q(Param("end")) + "))";
|
||
|
||
case "hours":
|
||
return "set(" + q(Param("target")) + ", hours(" + q(Param("start")) + ", " + q(Param("end")) + "))";
|
||
|
||
case "copy":
|
||
return "copy(" + q(Param("source")) + ", " + q(Param("target")) + ")";
|
||
|
||
case "setValue":
|
||
return "set(" + q(Param("target")) + ", " + ValueLiteral(Param("text")) + ")";
|
||
|
||
case "clear":
|
||
return "clear(" + string.Join(", ", _fields.Select(q)) + ")";
|
||
|
||
case "readOnly":
|
||
{
|
||
var expression = ConditionExpression();
|
||
if (expression.Length == 0)
|
||
{
|
||
expression = "true";
|
||
}
|
||
|
||
var flag = Param("invert") == "true" ? "!(" + expression + ")" : expression;
|
||
return "readOnly(" + q(Param("target")) + ", " + flag + ")";
|
||
}
|
||
|
||
case "notify":
|
||
return "notify(" + q(Param("message")) + ")";
|
||
|
||
case "ask":
|
||
return "if (!ask(" + q(Param("message")) + ")) return";
|
||
|
||
case "openUrl":
|
||
return Param("target").Length > 0 && Param("target") != "_blank"
|
||
? "openUrl(" + q(Param("url")) + ", " + q(Param("target")) + ")"
|
||
: "openUrl(" + q(Param("url")) + ")";
|
||
|
||
case "apiToField":
|
||
return "set(" + q(Param("target")) + ", await api(" + q(Param("url")) +
|
||
(Param("path").Length > 0 ? ", " + q(Param("path")) : string.Empty) + "))";
|
||
|
||
case "location":
|
||
return "await geoTo(" + q(Param("lat")) + ", " + q(Param("lng")) +
|
||
(Param("accuracy").Length > 0 ? ", " + q(Param("accuracy")) : string.Empty) + ")";
|
||
|
||
case "custom":
|
||
{
|
||
// TS ile ayni: yalnizca sondaki tek ';' silinir.
|
||
var code = Param("code").Trim();
|
||
return code.EndsWith(';') ? code[..^1] : code;
|
||
}
|
||
|
||
default:
|
||
return string.Empty;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// <c>true</c>/<c>false</c> mantiksal sabit olarak yazilir; metin olarak (<c>tpl('false')</c>) yazilsa
|
||
/// onay kutusu dolu sayardi. Diger degerler sablondur. scriptRecipes.ts <c>valueLiteral</c> ile ayni.
|
||
/// </summary>
|
||
private static string ValueLiteral(string text)
|
||
{
|
||
var trimmed = (text ?? string.Empty).Trim();
|
||
return trimmed.Equals("true", System.StringComparison.OrdinalIgnoreCase) || trimmed.Equals("false", System.StringComparison.OrdinalIgnoreCase)
|
||
? trimmed.ToLowerInvariant()
|
||
: "tpl(" + EditorScript.Quote(text) + ")";
|
||
}
|
||
|
||
private static string TriggerName(EditorScriptTrigger trigger)
|
||
{
|
||
switch (trigger)
|
||
{
|
||
case EditorScriptTrigger.Change: return "change";
|
||
case EditorScriptTrigger.Open: return "open";
|
||
default: return "both";
|
||
}
|
||
}
|
||
}
|