bchavez / Bogus

:card_index: A simple fake data generator for C#, F#, and VB.NET. Based on and ported from the famed faker.js.
Other
8.68k stars 495 forks source link

Merge all RuleSets into a single Dictionnary before populating #361

Closed tetris69 closed 3 years ago

tetris69 commented 3 years ago

Hi, thanks for building and maintaining this awesome tool. Here's my issue.

When defining two rules for the same property in the default RuleSet, the second rule replaces the first one, without changing the order of execution. This is great.

I would like the same behavior to happen when defining two rules for the same property, but in different rulesets. In the following example, I would like my client to have it's DatSecond property to "Y" .

var clientGenerator = new Faker<Client>()
                .RuleFor(o => o.DatFirst, f => "A")
                .RuleFor(o => o.DatSecond, (f, o) => o.DatFirst == "A" ? "X" : "Y");

clientGenerator.RuleSet("use_b", r =>
            {
                r.RuleFor(o => o.DatFirst, f => "B");
            });

var client = clientGenerator.Generate("default, use_b");
// ASSERT: client.DatSecond == "Y"

The goal is to have DatSecond populated no matter if the ruleset use_b is used, and also to not repeat DatSecond's rule.

bchavez commented 3 years ago

Hi @tetris69,

Pretty interesting scenario. The essence of the problem almost feels like a computed property that should get re-evaluated when DatFirst changes. I don't think there's any realistic way for me to detect the situation programmatically or even a way to build some kind of dependency tree to evaluate when things need to be re-computed.

For now, I think you'll have to resort to moving those "computed property" rules last; or in their own rule set and execute them after your "primary/raw data" (un-computed) properties are set.

Here's an example:

void Main()
{
   var clientFaker = new Faker<Client>()
                .RuleFor(o => o.DatFirst, f => "A")
                .RuleSet("use_b", r =>
                 {
                   r.RuleFor(o => o.DatFirst, f => "B");
                 })
                .FinishWith((f, o) => {
                   o.DatSecond = o.DatFirst == "A" ? "X" : "Y";
                });

   var client = clientFaker.Generate("default, use_b");
   client.Dump();
}

public class Client
{
   public string DatFirst;
   public string DatSecond;
}

image

In the code above, I used .FinishWith, but you could have a finalize rule set that does the same thing. Up to your coding style and preference of course.

Also, you could extend Bogus' Faker<T> with something like ComputedFaker<T> : Faker<T> that could automate the scenario. I'd have to think about it a little more.

bchavez commented 3 years ago

Here's another way to merge rules using MergeFaker<T>:

void Main()
{
      var clientFaker = new MergeFaker<Client>()
                .RuleFor(o => o.DatFirst, f => "A")
                .RuleFor(o => o.DatSecond, (f, o) => o.DatFirst == "A" ? "X" : "Y")
                .RuleSet("use_b", r =>
                 {
                    r.RuleFor(o => o.DatFirst, f => "B");
                 })
                 as MergeFaker<Client>;

   var mergedName = clientFaker.MergeRuleSets("default, use_b");

   var client = clientFaker.Generate(mergedName);
   client.Dump();
}

public class MergeFaker<T> : Faker<T> where T : class
{
   public string MergeRuleSets(string ruleSets = null)
   {
      var cleanRules = ParseDirtyRulesSets(ruleSets);
      var mergedName = Guid.NewGuid().ToString("n");

      this.RuleSet(mergedName, _ => {

         foreach (var ruleName in cleanRules)
         {
            if (!this.Actions.TryGetValue(ruleName, out var allRulesForSet)) continue;

            foreach(var (propName, rule) in allRulesForSet){
               AddRule(propName, rule.Action);
            }
         }
      });
      return mergedName;
   }
}

public class Client
{
   public string DatFirst;
   public string DatSecond;
}

image

The basic idea is,

tetris69 commented 3 years ago

Wow! :) I love your second solution. It works like a charm. I also added this function in my MergeFaker class to clear memory after using the merged ruleset.

public void DisposeRuleSet(string mergedName)
{
    this.Actions.Remove(mergedName);
}

Thank you very much and have a nice day.