Skip to content

Commit

Permalink
[FFM-5307] - .NET SDK - Fix for prereq identifier + update JSON parse…
Browse files Browse the repository at this point in the history
…r to use latest tests (#47)

What
When a feature depends on another prereq feature being true, the evaluation fails if the value and identifier are different values in the prereq. Update code to use latest ff-test-cases JSON format.
Some additional fixes were needed including Null reference exception and incorrect OR handling in groups when latest JSON was used.

Why
The checkPreRequisite() method incorrectly uses the value of the prereq instead of the identifier.

Testing
Tested locally with new ff-test-cases JSON + sanity checks with example program
  • Loading branch information
andybharness committed Nov 29, 2022
1 parent ee01744 commit 11eca6f
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 69 deletions.
13 changes: 7 additions & 6 deletions client/api/Evaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,10 @@ public string StringVariation(string key, dto.Target target, string defaultValue
private bool checkPreRequisite(FeatureConfig parentFeatureConfig, dto.Target target)
{
bool result = true;
List<Prerequisite> prerequisites = parentFeatureConfig.Prerequisites.ToList();
if ( prerequisites != null && prerequisites.Count > 0)
if (parentFeatureConfig.Prerequisites != null && parentFeatureConfig.Prerequisites.Count > 0)
{
List<Prerequisite> prerequisites = parentFeatureConfig.Prerequisites.ToList();

foreach (Prerequisite pqs in prerequisites)
{
FeatureConfig preReqFeatureConfig = this.repository.GetFlag(pqs.Feature);
Expand All @@ -105,7 +106,7 @@ private bool checkPreRequisite(FeatureConfig parentFeatureConfig, dto.Target tar
}

List<string> validPreReqVariations = pqs.Variations.ToList();
if (!validPreReqVariations.Contains(preReqEvaluatedVariation.Value))
if (!validPreReqVariations.Contains(preReqEvaluatedVariation.Identifier))
{
return false;
}
Expand Down Expand Up @@ -227,11 +228,11 @@ private bool IsTargetIncludedOrExcludedInSegment(List<string> segmentList, dto.T
return true;
}

// if we have rules, all should pass
// if we have rules, at least one should pass
if (segment.Rules != null)
{
Clause firstFailure = segment.Rules.FirstOrDefault(r => EvaluateClause(r, target) == false);
return firstFailure == null;
Clause firstSuccess = segment.Rules.FirstOrDefault(r => EvaluateClause(r, target));
return firstSuccess != null;
}
}
}
Expand Down
159 changes: 122 additions & 37 deletions tests/ff-server-sdk-test/EvaluatorTest.cs
Original file line number Diff line number Diff line change
@@ -1,29 +1,47 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using io.harness.cfsdk.client.api;
using io.harness.cfsdk.client.cache;
using io.harness.cfsdk.client.dto;
using io.harness.cfsdk.HarnessOpenAPIService;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NUnit.Framework;

[SetUpFixture]
public class SetupTracing
{
[OneTimeSetUp]
public void StartTest()
{
Trace.Listeners.Add(new ConsoleTraceListener());
}

[OneTimeTearDown]
public void EndTest()
{
Trace.Flush();
}
}

namespace ff_server_sdk_test
{
public class EvaluatorListener : IEvaluatorCallback
{
public void evaluationProcessed(FeatureConfig featureConfig, io.harness.cfsdk.client.dto.Target target, Variation variation)
public void evaluationProcessed(FeatureConfig featureConfig, io.harness.cfsdk.client.dto.Target target,
Variation variation)
{
var targetName = target != null ? target.Name : "_no_target";
Serilog.Log.Information($"processEvaluation {featureConfig.Feature}, {targetName}, {variation.Value} ");
}
}



[TestFixture]
public class EvaluatorTest
{
private static List<TestModel> testData = new List<TestModel>();
private static IRepository repository;
private static ICache cache;

Expand All @@ -37,73 +55,140 @@ static EvaluatorTest()
cache = new FeatureSegmentCache();
repository = new StorageRepository(cache, null, null);
evaluator = new Evaluator(repository, listener);
}

private static void LoadSegments(List<Segment> segments)
{
if (segments != null)
{
segments.ForEach(segment => { repository.SetSegment(segment.Identifier, segment); });
}
}

Assert.DoesNotThrow(() =>
private static void LoadFlags(List<FeatureConfig> flags)
{
if (flags != null)
{
foreach (string fileName in Directory.GetFiles("./ff-test-cases/tests", "*.json"))
flags.ForEach(flag => { repository.SetFlag(flag.Feature, flag); });
}
}


private static FeatureConfig FindFeatureConfig(string flagName, List<FeatureConfig> flags)
{
foreach (FeatureConfig nextFlag in flags)
{
if (nextFlag.Feature.Equals(flagName))
{
var testModel = JsonConvert.DeserializeObject<TestModel>(File.ReadAllText(fileName));
Assert.NotNull(testModel);
return nextFlag;
}
}
Assert.Fail("Could not find feature flag " + flagName);
return null;
}

string name = Path.GetFileName(fileName);
string feature = testModel.flag.Feature + name;
testModel.flag.Feature = feature;
testModel.testFile = name;

testData.Add(testModel);
private static List<string> GetTree(string path, string searchPattern)
{
List<string> found = new List<string>();
foreach (string file in Directory.GetFiles(path, searchPattern))
{
found.Add(file);
}

repository.SetFlag(testModel.flag.Feature, testModel.flag);
if (testModel.segments != null)
{
testModel.segments.ForEach(s =>
{
repository.SetSegment(s.Identifier, s);
});
}
}
});
foreach (string dir in Directory.GetDirectories(path, "*", SearchOption.TopDirectoryOnly))
{
List<string> subFound = GetTree(dir, searchPattern);
found.AddRange(subFound);
}

return found;
}

private static IEnumerable<TestCaseData> GenerateTestCases()
{
string baseTestPath = Path.GetFullPath("./ff-test-cases/tests/");

foreach (var test in testData)
foreach (string fileName in GetTree(baseTestPath, "*"))
{
foreach (var item in test.expected)
Console.WriteLine("Processing " + fileName);

var testModel = JsonConvert.DeserializeObject<TestModel>(File.ReadAllText(fileName),
new JsonSerializerSettings
{
ContractResolver = new LenientContractResolver()
});

Assert.NotNull(testModel);

foreach (Dictionary<string, object> nextTest in testModel.tests)
{
yield return new TestCaseData(test.flag.Feature, item.Key, item.Value, test);
object expected = nextTest.GetValueOrDefault("expected");
string target = (string)nextTest.GetValueOrDefault("target", null); // May be null
string flag = (string)nextTest.GetValueOrDefault("flag");
FeatureConfig feature = FindFeatureConfig(flag, testModel.flags);

LoadSegments(testModel.segments);
LoadFlags(testModel.flags);

string nUnitTestName = fileName.Replace(baseTestPath, "ff-test-cases ").Replace(".json", "");

nUnitTestName += "__with_flag_" + flag;
if (target != null)
{
nUnitTestName += "__with_target_" + target;
}

yield return new TestCaseData(nUnitTestName, target, expected, flag, feature.Kind, testModel);
}
}
}

[Test, Category("Evaluation Testing"), TestCaseSource("GenerateTestCases")]
public void ExecuteTestCases(string f, string identifier, bool result, TestModel test)
public void ExecuteTestCases(string testName, string targetIdentifier, object expected, string featureFlag, FeatureConfigKind kind, TestModel testModel)
{
io.harness.cfsdk.client.dto.Target target = null;
if (!identifier.Equals(noTarget))
if (!noTarget.Equals(targetIdentifier))
{
if (test.targets != null)
if (testModel.targets != null)
{
target = test.targets.Find(t => { return t.Identifier == identifier; });
target = testModel.targets.Find(t => { return t.Identifier == targetIdentifier; });
}
}
string feature = test.flag.Feature;
switch (test.flag.Kind)

object got = null;

switch (kind)
{
case FeatureConfigKind.Boolean:
bool res = evaluator.BoolVariation(feature, target, false);
Assert.AreEqual(res, result, $"Expected result for {feature} was {result}");
got = evaluator.BoolVariation(featureFlag, target, false);
break;
case FeatureConfigKind.Int:
double resInt = evaluator.NumberVariation(feature, target, 0);
got = evaluator.NumberVariation(featureFlag, target, 0);
break;
case FeatureConfigKind.String:
string resStr = evaluator.StringVariation(feature, target, "");
got = evaluator.StringVariation(featureFlag, target, "");
break;
case FeatureConfigKind.Json:
JObject resObj = evaluator.JsonVariation(feature, target, JObject.Parse("{val: 'default value'}"));
got = evaluator.JsonVariation(featureFlag, target, JObject.Parse("{val: 'default value'}"));
break;
}

Debug.Print(" TEST : {0}", testName);
Debug.Print(" FLAG : {0}", featureFlag);
Debug.Print(" TARGET : {0} ", targetIdentifier ?? "(none)");
Debug.Print("EXPECTED : {0} ({1})", expected, expected.GetType().Name);
Debug.Print(" GOT : {0} ({1})", got.ToString().Replace("\n", ""), got.GetType().Name);

if (kind == FeatureConfigKind.Json)
{
var expectedJson = JObject.Parse((string)expected);
Assert.AreEqual(expectedJson, got, $"Expected result for {featureFlag} was {expected}");
}
else
{
Assert.AreEqual(expected, got, $"Expected result for {featureFlag} was {expected}");
}
}

}
Expand Down
29 changes: 27 additions & 2 deletions tests/ff-server-sdk-test/TestModel.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,43 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using io.harness.cfsdk.HarnessOpenAPIService;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

namespace ff_server_sdk_test
{
public class LenientContractResolver : DefaultContractResolver
{
protected override JsonProperty CreateProperty(System.Reflection.MemberInfo member, MemberSerialization memberSerialization)
{
var property = base.CreateProperty(member, memberSerialization);
property.Required = Required.Default;
return property;
}

protected override JsonObjectContract CreateObjectContract(Type objectType)
{
var contract = base.CreateObjectContract(objectType);
contract.ItemRequired = Required.Default;
return contract;
}

protected override JsonProperty CreatePropertyFromConstructorParameter(JsonProperty matchingMemberProperty, ParameterInfo parameterInfo)
{
var property = base.CreatePropertyFromConstructorParameter(matchingMemberProperty, parameterInfo);
property.Required = Required.Default;
return property;
}
}

public class TestModel
{
public string testFile;
public FeatureConfig flag;
public List<FeatureConfig> flags;
public List<io.harness.cfsdk.client.dto.Target> targets;
public List<Segment> segments;
public Dictionary<string, bool> expected;
public List<Dictionary<string, object>> tests;
public TestModel()
{
}
Expand Down
25 changes: 2 additions & 23 deletions tests/ff-server-sdk-test/ff-server-sdk-test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,30 +25,9 @@
<None Remove="Microsoft.NET.Test.Sdk" />
</ItemGroup>
<ItemGroup>
<None Update="ff-test-cases\tests\on_off_no_rules.json">
<Content Include="ff-test-cases\tests\**\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="ff-test-cases\tests\off_flag.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="ff-test-cases\tests\segment_varmap_target.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="ff-test-cases\tests\segment_includes_target.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="ff-test-cases\tests\rules_priority.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="ff-test-cases\tests\bool_on_simple_rule.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="ff-test-cases\tests\prereq.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="ff-test-cases\tests\off_off_no_rules.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\ff-netF48-server-sdk.csproj" />
Expand Down
2 changes: 1 addition & 1 deletion tests/ff-server-sdk-test/ff-test-cases
Submodule ff-test-cases updated 93 files
+22 −0 .github/workflows/ci.yml
+1 −0 .gitignore
+100 −0 Makefile
+134 −0 README.md
+617 −0 client-v1.yaml
+3 −0 go.mod
+615 −0 tests/DefaultOffRules/DefaultOffRules_Type_Bool_State_Disabled_On_False_Off_False_Should_Return_False.json
+615 −0 tests/DefaultOffRules/DefaultOffRules_Type_Bool_State_Disabled_On_False_Off_True_Should_Return_True.json
+615 −0 tests/DefaultOffRules/DefaultOffRules_Type_Bool_State_Disabled_On_True_Off_False_Should_Return_False.json
+615 −0 tests/DefaultOffRules/DefaultOffRules_Type_Bool_State_Disabled_On_True_Off_True_Should_Return_True.json
+621 −0 ...OffRules/DefaultOffRules_Type_JSON_State_Disabled_On_complexJSON_Off_emptyJSON_Should_Return_validJSON.json
+621 −0 ...ltOffRules/DefaultOffRules_Type_JSON_State_Disabled_On_validJSON_Off_emptyJSON_Should_Return_emptyJSON.json
+621 −0 tests/DefaultOffRules/DefaultOffRules_Type_Number_State_Disabled_On_011_Off_2_Should_Return_2.json
+621 −0 tests/DefaultOffRules/DefaultOffRules_Type_Number_State_Disabled_On_324523_Off_011_Should_Return_011.json
+621 −0 ...ultOffRules/DefaultOffRules_Type_String_State_Disabled_On_sonnet19_Off_sonnet53_Should_Return_sonnet53.json
+621 −0 ...ultOffRules/DefaultOffRules_Type_String_State_Disabled_On_sonnet53_Off_sonnet19_Should_Return_sonnet19.json
+615 −0 tests/DefaultOnRules/DefaultOnRules_Type_Bool_State_Enabled_On_False_Off_False_Should_Return_False.json
+615 −0 tests/DefaultOnRules/DefaultOnRules_Type_Bool_State_Enabled_On_False_Off_True_Should_Return_True.json
+615 −0 tests/DefaultOnRules/DefaultOnRules_Type_Bool_State_Enabled_On_True_Off_False_Should_Return_False.json
+615 −0 tests/DefaultOnRules/DefaultOnRules_Type_Bool_State_Enabled_On_True_Off_True_Should_Return_True.json
+621 −0 ...tOnRules/DefaultOnRules_Type_JSON_State_Enabled_On_complexJSON_Off_emptyJSON_Should_Return_complexJSON.json
+621 −0 ...faultOnRules/DefaultOnRules_Type_JSON_State_Enabled_On_validJSON_Off_emptyJSON_Should_Return_validJSON.json
+621 −0 tests/DefaultOnRules/DefaultOnRules_Type_Number_State_Enabled_On_011_Off_2_Should_Return_011.json
+621 −0 tests/DefaultOnRules/DefaultOnRules_Type_Number_State_Enabled_On_324523_Off_011_Should_Return_324523.json
+621 −0 ...efaultOnRules/DefaultOnRules_Type_String_State_Enabled_On_sonnet19_Off_sonnet53_Should_Return_sonnet19.json
+621 −0 ...efaultOnRules/DefaultOnRules_Type_String_State_Enabled_On_sonnet53_Off_sonnet19_Should_Return_sonnet53.json
+634 −0 tests/GroupRules/GroupRules_Type_Bool_State_Enabled_EqualsThree_Should_Return_false_for_Target_three.json
+634 −0 tests/GroupRules/GroupRules_Type_Bool_State_Enabled_EqualsThree_Should_Return_true_for_Target_one.json
+634 −0 ...pRules/GroupRules_Type_Bool_State_Enabled_GroupWithMultipleOrRules_Should_Return_true_for_Target_three.json
+634 −0 tests/GroupRules/GroupRules_Type_Bool_State_Enabled_InOneTwoThree_Should_Return_true_for_Target_three.json
+634 −0 ...oupRules/GroupRules_Type_Bool_State_Enabled_IncludeAndExcludeGroup_Should_Return_true_for_Target_three.json
+634 −0 tests/GroupRules/GroupRules_Type_Bool_State_Enabled_IncludedOnly_Should_Return_true_for_Target_three.json
+634 −0 ...pRules/GroupRules_Type_Bool_State_Enabled_StartsWithTExcludesThree_Should_Return_true_for_Target_three.json
+634 −0 tests/GroupRules/GroupRules_Type_Bool_State_Enabled_StartsWithT_Should_Return_false_for_Target_one.json
+634 −0 tests/GroupRules/GroupRules_Type_Bool_State_Enabled_StartsWithT_Should_Return_true_for_Target_three.json
+622 −0 ...rerequisites/Type_BoolBool_State_Enabled_On_True_Off_False_Prerequisites_AlwaysOff_Should_Return_False.json
+622 −0 ...Prerequisites/Type_BoolBool_State_Enabled_On_True_Off_False_Prerequisites_AlwaysOff_Should_Return_True.json
+622 −0 .../Prerequisites/Type_BoolBool_State_Enabled_On_True_Off_True_Prerequisites_AlwaysOn_Should_Return_False.json
+622 −0 ...s/Prerequisites/Type_BoolBool_State_Enabled_On_True_Off_True_Prerequisites_AlwaysOn_Should_Return_True.json
+622 −0 ...Prerequisites/Type_BoolInt_State_Enabled_On_True_Off_False_Prerequisites_AlwaysOff_Should_Return_False.json
+622 −0 .../Prerequisites/Type_BoolInt_State_Enabled_On_True_Off_False_Prerequisites_AlwaysOff_Should_Return_True.json
+622 −0 ...s/Prerequisites/Type_BoolInt_State_Enabled_On_True_Off_True_Prerequisites_AlwaysOn_Should_Return_False.json
+622 −0 tests/Prerequisites/Type_BoolInt_State_Enabled_On_True_Off_True_Prerequisites_AlwaysOn_Should_Return_True.json
+628 −0 tests/Prerequisites/Type_IntBool_State_Enabled_On_2_Off_011_Prerequisites_AlwaysOff_Should_Return_011.json
+628 −0 tests/Prerequisites/Type_IntBool_State_Enabled_On_2_Off_011_Prerequisites_AlwaysOff_Should_Return_2.json
+628 −0 tests/Prerequisites/Type_IntBool_State_Enabled_On_2_Off_011_Prerequisites_AlwaysOn_Should_Return_011.json
+628 −0 tests/Prerequisites/Type_IntBool_State_Enabled_On_2_Off_011_Prerequisites_AlwaysOn_Should_Return_2.json
+628 −0 tests/Prerequisites/Type_IntInt_State_Enabled_On_2_Off_011_Prerequisites_AlwaysOff_Should_Return_011.json
+628 −0 tests/Prerequisites/Type_IntInt_State_Enabled_On_2_Off_011_Prerequisites_AlwaysOff_Should_Return_2.json
+628 −0 tests/Prerequisites/Type_IntInt_State_Enabled_On_2_Off_011_Prerequisites_AlwaysOn_Should_Return_011.json
+628 −0 tests/Prerequisites/Type_IntInt_State_Enabled_On_2_Off_011_Prerequisites_AlwaysOn_Should_Return_2.json
+628 −0 tests/Prerequisites/Type_IntJson_State_Enabled_On_2_Off_011_Prerequisites_AlwaysOff_Should_Return_011.json
+628 −0 tests/Prerequisites/Type_IntJson_State_Enabled_On_2_Off_011_Prerequisites_AlwaysOff_Should_Return_2.json
+628 −0 tests/Prerequisites/Type_IntJson_State_Enabled_On_2_Off_011_Prerequisites_AlwaysOn_Should_Return_011.json
+628 −0 tests/Prerequisites/Type_IntJson_State_Enabled_On_2_Off_011_Prerequisites_AlwaysOn_Should_Return_2.json
+628 −0 tests/Prerequisites/Type_IntString_State_Enabled_On_2_Off_011_Prerequisites_AlwaysOff_Should_Return_011.json
+628 −0 tests/Prerequisites/Type_IntString_State_Enabled_On_2_Off_011_Prerequisites_AlwaysOff_Should_Return_2.json
+628 −0 tests/Prerequisites/Type_IntString_State_Enabled_On_2_Off_011_Prerequisites_AlwaysOn_Should_Return_011.json
+628 −0 tests/Prerequisites/Type_IntString_State_Enabled_On_2_Off_011_Prerequisites_AlwaysOn_Should_Return_2.json
+628 −0 tests/Prerequisites/Type_JsonJson_State_Enabled_On_com_Off_val_Prerequisites_AlwaysOff_Should_Return_Com.json
+628 −0 tests/Prerequisites/Type_JsonJson_State_Enabled_On_com_Off_val_Prerequisites_AlwaysOff_Should_Return_Val.json
+628 −0 tests/Prerequisites/Type_JsonJson_State_Enabled_On_com_Off_val_Prerequisites_AlwaysOn_Should_Return_Com.json
+628 −0 tests/Prerequisites/Type_JsonJson_State_Enabled_On_com_Off_val_Prerequisites_AlwaysOn_Should_Return_Val.json
+628 −0 tests/Prerequisites/Type_StrStr_State_Enabled_On_s19_Off_s53_Prerequisites_AlwaysOff_Should_Return_off.json
+628 −0 tests/Prerequisites/Type_StrStr_State_Enabled_On_s19_Off_s53_Prerequisites_AlwaysOff_Should_Return_on.json
+628 −0 tests/Prerequisites/Type_StrStr_State_Enabled_On_s19_Off_s53_Prerequisites_AlwaysOn_Should_Return_Off.json
+628 −0 tests/Prerequisites/Type_StrStr_State_Enabled_On_s19_Off_s53_Prerequisites_AlwaysOn_Should_Return_On.json
+638 −0 tests/TargetRules/TargetRules_Type_Bool_State_Disabled_Should_Return_Default_Off_Value.json
+638 −0 tests/TargetRules/TargetRules_Type_Bool_State_Enabled_Should_Return_false_for_Target_four.json
+659 −0 tests/TargetRules/TargetRules_Type_Bool_State_Enabled_Should_Return_false_for_Target_six.json
+638 −0 tests/TargetRules/TargetRules_Type_Bool_State_Enabled_Should_Return_true_for_Target_three.json
+665 −0 tests/TargetRules/TargetRules_Type_JSON_State_Enabled_Should_Return_complexJSON_for_Target_five.json
+665 −0 tests/TargetRules/TargetRules_Type_JSON_State_Enabled_Should_Return_emptyJSON_for_Target_four.json
+665 −0 tests/TargetRules/TargetRules_Type_JSON_State_Enabled_Should_Return_validJSON_for_Target_anonymous.json
+665 −0 tests/TargetRules/TargetRules_Type_Number_State_Enabled_Should_Return_2_for_Target_four.json
+665 −0 tests/TargetRules/TargetRules_Type_Number_State_Enabled_Should_Return_324523_for_Target_five.json
+665 −0 tests/TargetRules/TargetRules_Type_Number_State_Enabled_Should_Return_sonnet53_for_Target_anonymous.json
+665 −0 tests/TargetRules/TargetRules_Type_String_State_Enabled_Should_Return_sonnet53_for_Target_anonymous.json
+665 −0 tests/TargetRules/TargetRules_Type_String_State_Enabled_Should_Return_sonnet53_for_Target_four.json
+665 −0 tests/TargetRules/TargetRules_Type_String_State_Enabled_Should_Return_thehobbit_for_Target_five.json
+49 −46 tests/bool_on_simple_rule.json
+33 −30 tests/off_flag.json
+33 −30 tests/off_off_no_rules.json
+33 −30 tests/on_off_no_rules.json
+96 −48 tests/prereq.json
+63 −60 tests/rules_priority.json
+48 −45 tests/segment_includes_target.json
+0 −78 tests/segment_varmap_target.json
+6 −6 tests/test_empty_or_missing_target_attributes.json
+215 −0 types.gen.go
+16 −0 types.go
+121 −0 validator.go
+10 −0 validator_test.go

0 comments on commit 11eca6f

Please sign in to comment.