diff --git a/cumulusci.yml b/cumulusci.yml
index 64e8a897..ddab7b81 100644
--- a/cumulusci.yml
+++ b/cumulusci.yml
@@ -3,6 +3,7 @@ project:
name: Snowfakery
dependencies:
- github: https://github.com/SalesforceFoundation/NPSP
+ source_format: sfdx
sources:
npsp:
diff --git a/examples/multiselect.yml b/examples/multiselect.yml
new file mode 100644
index 00000000..9b2cf17a
--- /dev/null
+++ b/examples/multiselect.yml
@@ -0,0 +1,18 @@
+- plugin: snowfakery.standard_plugins.experimental.multiselect.Multiselect
+- object: DNA
+ fields:
+ with_replacement:
+ Multiselect.random_choices:
+ min: 0
+ max: 10
+ with_replacement: True
+ choices: A,C,G,T
+ no_replacement:
+ Multiselect.random_choices:
+ min: 0
+ max: 10
+ with_replacement: False
+ choices: A,C,G,T
+ defaults: # no replacement, min=1, max=len(choices)
+ Multiselect.random_choices:
+ choices: A,C,G,T
diff --git a/examples/salesforce/custom_field_multiselect.yml b/examples/salesforce/custom_field_multiselect.yml
new file mode 100644
index 00000000..15948b9d
--- /dev/null
+++ b/examples/salesforce/custom_field_multiselect.yml
@@ -0,0 +1,12 @@
+# use `cci task run dx_push` to push the appropriate metadata
+
+- plugin: snowfakery.standard_plugins.experimental.multiselect.Multiselect
+- object: Contact
+ fields:
+ FirstName:
+ fake: FirstName
+ LastName:
+ fake: LastName
+ Types__c:
+ Multiselect.random_choices:
+ choices: Donor;Volunteer;Staff Member
diff --git a/force-app/main/default/objects/Contact/fields/Types__c.field-meta.xml b/force-app/main/default/objects/Contact/fields/Types__c.field-meta.xml
new file mode 100644
index 00000000..57597b2a
--- /dev/null
+++ b/force-app/main/default/objects/Contact/fields/Types__c.field-meta.xml
@@ -0,0 +1,31 @@
+
+
+ Types__c
+ false
+
+ false
+ false
+ MultiselectPicklist
+
+ true
+
+ false
+
+ Donor
+ false
+
+
+
+ Staff Member
+ false
+
+
+
+ Volunteer
+ false
+
+
+
+
+ 3
+
diff --git a/force-app/main/default/profiles/Admin.profile-meta.xml b/force-app/main/default/profiles/Admin.profile-meta.xml
new file mode 100644
index 00000000..521262de
--- /dev/null
+++ b/force-app/main/default/profiles/Admin.profile-meta.xml
@@ -0,0 +1,9 @@
+
+
+ false
+
+ true
+ Contact.Types__c
+ true
+
+
diff --git a/sfdx-project.json b/sfdx-project.json
new file mode 100644
index 00000000..60fec5ce
--- /dev/null
+++ b/sfdx-project.json
@@ -0,0 +1,12 @@
+{
+ "packageDirectories": [
+ {
+ "path": "force-app",
+ "default": true
+ }
+ ],
+ "name": "snowfakery-demo",
+ "namespace": "",
+ "sfdcLoginUrl": "https://login.salesforce.com",
+ "sourceApiVersion": "51.0"
+}
diff --git a/snowfakery/standard_plugins/experimental/multiselect.py b/snowfakery/standard_plugins/experimental/multiselect.py
new file mode 100644
index 00000000..b219fca7
--- /dev/null
+++ b/snowfakery/standard_plugins/experimental/multiselect.py
@@ -0,0 +1,29 @@
+import random
+import builtins
+
+from snowfakery import SnowfakeryPlugin
+
+
+# This is experimental primarily because it hard-codes the separator.
+# It would be superior for the output stream to select the separator.
+
+# Also not thrilled that it cannot accept a list as input.
+
+
+class Multiselect(SnowfakeryPlugin):
+ class Functions:
+ def random_choices(
+ self,
+ choices: str,
+ min: int = 1,
+ max: int = None,
+ with_replacement: bool = False,
+ separator=";",
+ ):
+ choices = choices.split(",")
+ max = max or len(choices)
+ num_choices = random.randint(min, builtins.min(max, len(choices)))
+ if with_replacement:
+ return ";".join(random.choices(choices, k=num_choices))
+ else:
+ return ";".join(random.sample(choices, k=num_choices))
diff --git a/tests/test_custom_plugins_and_providers.py b/tests/test_custom_plugins_and_providers.py
index ace34444..ec743768 100644
--- a/tests/test_custom_plugins_and_providers.py
+++ b/tests/test_custom_plugins_and_providers.py
@@ -2,6 +2,7 @@
import math
import operator
from base64 import b64decode
+from pathlib import Path
from snowfakery import SnowfakeryPlugin, lazy
from snowfakery.plugins import PluginResult
@@ -307,3 +308,9 @@ def test_null_attributes(self):
with pytest.raises(DataGenError) as e:
generate_data(StringIO(yaml))
assert 6 > e.value.line_num >= 3
+
+ def test_experimental_multiselect(self, generated_rows):
+ example = Path(__file__).parent.parent / "examples/multiselect.yml"
+ with open(example) as f:
+ generate_data(f)
+ assert all(ch in "ACGT;" for ch in generated_rows.row_values(0, "defaults"))