Skip to content

Commit 69fe485

Browse files
committed
fix(validator): don't flag flag-style HR tokens; round-trip Insert from URL without forcing an empty <Calculation>
Insert from URL renders its SelectAll boolean as the bare flag token 'Select' (present = True, absent = False) — same style as 'Verify SSL Certificates' and 'Don't encode URL'. The validator's positional match was checking 'Select' against SelectAll's ['On', 'Off'] and yellow- underlining it. Fix: when a bare token exactly equals an unused boolean/flag-param's HrLabel, treat it as a presence marker and consume the param without value validation. Falls through to positional enum validation for tokens that aren't flag labels. Also fixed InsertFromUrlStep.ToXml to skip the <Calculation> URL element when Url is null — matches the real FM Pro shapes in the user's sample where the base step contains only the four boolean flag children and no URL calc. Url and Target are now both optional, so a bare-bones Insert from URL round-trips byte-intact. Added regression tests covering: - Flag tokens not flagged by the validator. - Real-world Insert from URL shapes from the user's FM Pro export. - Display-line from typical shape validates cleanly end-to-end.
1 parent 5ee0bf4 commit 69fe485

4 files changed

Lines changed: 111 additions & 11 deletions

File tree

src/SharpFM.Model/Scripting/ScriptValidator.cs

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,14 +123,36 @@ public static List<ScriptDiagnostic> Validate(string displayText)
123123
break;
124124
}
125125

126-
// Second: positional match — consume the next available
127-
// param in order. Only validate the value when that
128-
// param has restricted values (enum/boolean). Non-enum
126+
// Flag-style match: a bare token that exactly equals
127+
// an unused boolean/flag param's HrLabel is a presence
128+
// marker (e.g. "Select", "Verify SSL Certificates",
129+
// "Append line feed"). Not a value — consume the
130+
// param and skip value validation.
131+
bool matchedFlag = false;
132+
if (!matchedLabel)
133+
{
134+
for (int pi = 0; pi < metadata.Params.Count; pi++)
135+
{
136+
if (usedParams[pi]) continue;
137+
var catalogParam = metadata.Params[pi];
138+
if (catalogParam.HrLabel is null) continue;
139+
if (catalogParam.Type is not ("boolean" or "flagBoolean" or "flagElement")) continue;
140+
if (!paramTrimmed.Equals(catalogParam.HrLabel, StringComparison.OrdinalIgnoreCase)) continue;
141+
142+
usedParams[pi] = true;
143+
matchedFlag = true;
144+
break;
145+
}
146+
}
147+
148+
// Positional match — consume the next available param
149+
// in order. Only validate the value when that param
150+
// has restricted values (enum/boolean). Non-enum
129151
// params (field, calc, text) accept anything, so we
130152
// must NOT keep searching past them looking for an
131153
// enum — that produced false-positive warnings on
132154
// field references like "Assets::Selected File".
133-
if (!matchedLabel && !LooksLikeCalculation(paramTrimmed))
155+
if (!matchedLabel && !matchedFlag && !LooksLikeCalculation(paramTrimmed))
134156
{
135157
for (int pi = 0; pi < metadata.Params.Count; pi++)
136158
{

src/SharpFM.Model/Scripting/Steps/InsertFromUrlStep.cs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public sealed class InsertFromUrlStep : ScriptStep, IStepFactory
1515
public bool VerifySslCertificates { get; set; }
1616
public bool DontEncodeUrl { get; set; }
1717
public FieldRef? Target { get; set; }
18-
public Calculation Url { get; set; }
18+
public Calculation? Url { get; set; }
1919
public Calculation? CurlOptions { get; set; }
2020

2121
public InsertFromUrlStep(
@@ -34,7 +34,7 @@ public InsertFromUrlStep(
3434
VerifySslCertificates = verifySslCertificates;
3535
DontEncodeUrl = dontEncodeUrl;
3636
Target = target;
37-
Url = url ?? new Calculation("");
37+
Url = url;
3838
CurlOptions = curlOptions;
3939
}
4040

@@ -50,7 +50,7 @@ public override XElement ToXml()
5050
new XElement("VerifySSLCertificates", new XAttribute("state", VerifySslCertificates ? "True" : "False")));
5151
if (CurlOptions is not null)
5252
step.Add(new XElement("CURLOptions", CurlOptions.ToXml("Calculation")));
53-
step.Add(Url.ToXml("Calculation"));
53+
if (Url is not null) step.Add(Url.ToXml("Calculation"));
5454
if (Target is not null)
5555
{
5656
if (Target.IsVariable) step.Add(new XElement("Text"));
@@ -65,9 +65,10 @@ public override string ToDisplayLine()
6565
if (SelectAll) parts.Add("Select");
6666
parts.Add($"With dialog: {(WithDialog ? "On" : "Off")}");
6767
if (Target is not null) parts.Add($"Target: {Target.ToDisplayString()}");
68-
parts.Add(Url.Text);
68+
if (Url is not null) parts.Add(Url.Text);
6969
if (VerifySslCertificates) parts.Add("Verify SSL Certificates");
7070
if (CurlOptions is not null) parts.Add($"cURL options: {CurlOptions.Text}");
71+
if (DontEncodeUrl) parts.Add("Don't encode URL");
7172
return $"Insert from URL [ {string.Join(" ; ", parts)} ]";
7273
}
7374

@@ -81,7 +82,7 @@ public override string ToDisplayLine()
8182
var curlEl = step.Element("CURLOptions")?.Element("Calculation");
8283
var curl = curlEl is not null ? Calculation.FromXml(curlEl) : null;
8384
var urlEl = step.Element("Calculation");
84-
var url = urlEl is not null ? Calculation.FromXml(urlEl) : new Calculation("");
85+
var url = urlEl is not null ? Calculation.FromXml(urlEl) : null;
8586
var fieldEl = step.Element("Field");
8687
var target = fieldEl is not null ? FieldRef.FromXml(fieldEl) : null;
8788
return new InsertFromUrlStep(selectAll, withDialog, verify, dontEncode, target, url, curl, enabled);
@@ -92,8 +93,9 @@ public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams)
9293
bool selectAll = false;
9394
bool withDialog = true;
9495
bool verify = false;
96+
bool dontEncode = false;
9597
FieldRef? target = null;
96-
Calculation url = new("");
98+
Calculation? url = null;
9799
Calculation? curl = null;
98100
bool urlSeen = false;
99101
foreach (var tok in hrParams)
@@ -103,6 +105,8 @@ public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams)
103105
selectAll = true;
104106
else if (t.Equals("Verify SSL Certificates", StringComparison.OrdinalIgnoreCase))
105107
verify = true;
108+
else if (t.Equals("Don't encode URL", StringComparison.OrdinalIgnoreCase))
109+
dontEncode = true;
106110
else if (t.StartsWith("With dialog:", StringComparison.OrdinalIgnoreCase))
107111
withDialog = t.Substring(12).Trim().Equals("On", StringComparison.OrdinalIgnoreCase);
108112
else if (t.StartsWith("Target:", StringComparison.OrdinalIgnoreCase))
@@ -115,7 +119,7 @@ public static ScriptStep FromDisplayParams(bool enabled, string[] hrParams)
115119
urlSeen = true;
116120
}
117121
}
118-
return new InsertFromUrlStep(selectAll, withDialog, verify, false, target, url, curl, enabled);
122+
return new InsertFromUrlStep(selectAll, withDialog, verify, dontEncode, target, url, curl, enabled);
119123
}
120124

121125
public static StepMetadata Metadata { get; } = new()

tests/SharpFM.Tests/Scripting/ScriptValidatorTests.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,4 +178,25 @@ public void PositionalEnum_StillFlaggedWhenInvalid()
178178
Assert.NotEmpty(diagnostics);
179179
Assert.Equal(DiagnosticSeverity.Warning, diagnostics[0].Severity);
180180
}
181+
182+
[Fact]
183+
public void InsertFromUrl_SelectFlagToken_NotFlagged()
184+
{
185+
// Regression: "Select" is a flag-style presence marker for the
186+
// SelectAll boolean param, not a value. The validator used to
187+
// check "Select" against ["On", "Off"] and fail.
188+
var script = "Insert from URL [ Select ; With dialog: Off ; $url ]";
189+
var diagnostics = ScriptValidator.Validate(script);
190+
Assert.Empty(diagnostics);
191+
}
192+
193+
[Fact]
194+
public void InsertFromUrl_AllFlagTokens_NotFlagged()
195+
{
196+
// Every flag token on Insert from URL is a bare HrLabel: "Select",
197+
// "Verify SSL Certificates". None should warn.
198+
var script = "Insert from URL [ Select ; With dialog: Off ; $url ; Verify SSL Certificates ]";
199+
var diagnostics = ScriptValidator.Validate(script);
200+
Assert.Empty(diagnostics);
201+
}
181202
}

tests/SharpFM.Tests/Scripting/Steps/InsertFromUrlStepTests.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,57 @@ public void Registry_HasStep()
2525
Assert.True(StepRegistry.ByName.TryGetValue("Insert from URL", out var metadata));
2626
Assert.Equal(160, metadata!.Id);
2727
}
28+
29+
[Theory]
30+
[InlineData(false, false, true, false, false, false, false)]
31+
[InlineData(true, false, false, false, false, false, false)]
32+
[InlineData(false, false, true, false, true, true, true)]
33+
[InlineData(true, false, true, false, true, true, true)]
34+
[InlineData(true, false, true, true, true, true, true)]
35+
[InlineData(true, true, true, true, true, true, true)]
36+
public void RealWorldShapes_RoundTrip(
37+
bool noInteract, bool dontEncode, bool selectAll, bool verifySsl,
38+
bool withCurlOptions, bool withUrl, bool withField)
39+
{
40+
// Covers the series of variants in the user's FM Pro sample: base,
41+
// base without Select, with calcs, set to variable, verify SSL on,
42+
// and DontEncodeURL on. Each must XML-round-trip byte-intact.
43+
var xml = new System.Text.StringBuilder();
44+
xml.Append("<Step enable=\"True\" id=\"160\" name=\"Insert from URL\">");
45+
xml.Append($"<NoInteract state=\"{(noInteract ? "True" : "False")}\" />");
46+
xml.Append($"<DontEncodeURL state=\"{(dontEncode ? "True" : "False")}\" />");
47+
xml.Append($"<SelectAll state=\"{(selectAll ? "True" : "False")}\" />");
48+
xml.Append($"<VerifySSLCertificates state=\"{(verifySsl ? "True" : "False")}\" />");
49+
if (withCurlOptions)
50+
xml.Append("<CURLOptions><Calculation><![CDATA[\"--user myuser:mypass\"]]></Calculation></CURLOptions>");
51+
if (withUrl)
52+
xml.Append("<Calculation><![CDATA[\"https://example.com/\" & $id]]></Calculation>");
53+
if (withField)
54+
{
55+
xml.Append("<Text />");
56+
xml.Append("<Field>$insertFromUrlDataResponseVar</Field>");
57+
}
58+
xml.Append("</Step>");
59+
60+
var source = XElement.Parse(xml.ToString());
61+
var step = InsertFromUrlStep.Metadata.FromXml!(source);
62+
Assert.True(XNode.DeepEquals(source, step.ToXml()),
63+
$"Round-trip mismatch.\nSource:\n{source}\n\nOutput:\n{step.ToXml()}");
64+
}
65+
66+
[Fact]
67+
public void Display_BaseShape_IsValidatorClean()
68+
{
69+
// Sanity: render the display line for a typical shape and feed
70+
// it back through the script validator — zero diagnostics expected.
71+
var source = XElement.Parse(
72+
"<Step enable=\"True\" id=\"160\" name=\"Insert from URL\">"
73+
+ "<NoInteract state=\"False\" /><DontEncodeURL state=\"False\" />"
74+
+ "<SelectAll state=\"True\" /><VerifySSLCertificates state=\"False\" />"
75+
+ "</Step>");
76+
var step = InsertFromUrlStep.Metadata.FromXml!(source);
77+
var display = step.ToDisplayLine();
78+
var diagnostics = SharpFM.Model.Scripting.ScriptValidator.Validate(display);
79+
Assert.Empty(diagnostics);
80+
}
2881
}

0 commit comments

Comments
 (0)