diff --git a/examples/salesforce/CommunityUsers.yml b/examples/salesforce/CommunityUsers.yml deleted file mode 100644 index c181aaf5..00000000 --- a/examples/salesforce/CommunityUsers.yml +++ /dev/null @@ -1,88 +0,0 @@ -- plugin: snowfakery.standard_plugins.Salesforce -- plugin: snowfakery.standard_plugins.Salesforce.SalesforceQuery - -- object: User - fields: - Alias: Grace - Username: - fake: Username - LastName: Wong - Email: ${{Username}} - TimeZoneSidKey: America/Bogota - LocaleSidKey: en_US - EmailEncodingKey: UTF-8 - LanguageLocaleKey: en_US - ProfileId: - Salesforce.ProfileId: Identity User - joins_from: - - object: PermissionSetAssignment - join_field: AssigneeId - to: - - PermissionSetId: - Salesforce.PermissionSet: ActionPlans - - PermissionSetId: - Salesforce.PermissionSet: VoiceInbound - - PermissionSetId: - Salesforce.PermissionSet: DocumentChecklist - - friends: - - object: PermissionSetAssignment - fields: - AssigneeId: - reference: User - PermissionSetId: - SalesforceQuery.find_record: - from: PermissionSet - where: Name='ActionPlans' - - object: PermissionSetAssignment - fields: - AssigneeId: - reference: User - PermissionSetId: - SalesforceQuery.find_record: - from: PermissionSet - where: Name='ActionPlans' - - # __permissionSets: - # Salesforce.PermissionSetAssignments: - # names: ActionPlans,CallCoachingUser - # - object: __junk_wrapper - # friends: - # - object: PermissionSetAssignment - # fields: - # AssigneeId: - # reference: User - # PermissionSetId: - # SalesforceQuery.find_record: - # query_from: PermissionSet where Name='ActionPlans' - # - object: PermissionSetAssignment - # fields: - # AssigneeId: - # reference: User - # PermissionSetId: - # SalesforceQuery.find_record: - # query_from: PermissionSet where Name='ActionPlans' - # - object: PermissionSetAssignment - - # fields: - # AssigneeId: - # reference: User - # PermissionSetId: - # SalesforceQuery.find_record: - # query_from: PermissionSet where Name='CallCoachingUser' -- object: User - nickname: RandomizedUser - fields: - Username: - fake: Username - LastName: - fake: last_name - Email: - fake: email - Alias: Grace - TimeZoneSidKey: America/Bogota - LocaleSidKey: en_US - EmailEncodingKey: UTF-8 - LanguageLocaleKey: en_US - ProfileId: - Salesforce.ProfileId: Identity User diff --git a/requirements/dev.in b/requirements/dev.in index 973615d7..0c1fb616 100644 --- a/requirements/dev.in +++ b/requirements/dev.in @@ -13,4 +13,5 @@ tox tox-gh-actions # needed for CI only pytest-vcr vcrpy -responses \ No newline at end of file +responses +jsonschema \ No newline at end of file diff --git a/requirements/dev.txt b/requirements/dev.txt index ed996e58..5f54729b 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -71,6 +71,9 @@ jinja2==2.11.3 # via # -r requirements/prod.txt # mkdocs +jsonschema==4.2.1 + # via + # -r requirements/dev.txt markdown==3.3.6 # via mkdocs markupsafe==2.0.1 diff --git a/schema/example.recipe.yml b/schema/example.recipe.yml new file mode 100644 index 00000000..0efef5e1 --- /dev/null +++ b/schema/example.recipe.yml @@ -0,0 +1,17 @@ +# yaml-language-server: $schema=snowfakery_recipe.jsonschema.json +- include_file: foo.yml +- object: Account + nickname: An ice sculpture + count: abcdee + fields: + a: blah + friends: + - object: q +- object: Contact + nickname: A blue mouse + count: 5 +- var: xxxx + value: jiojoioj + fjkioesjkf: fjeiosjfosi + fioesjfoi: fjieosjfso + diff --git a/schema/snowfakery_recipe.jsonschema.json b/schema/snowfakery_recipe.jsonschema.json new file mode 100644 index 00000000..ee8c13d7 --- /dev/null +++ b/schema/snowfakery_recipe.jsonschema.json @@ -0,0 +1,235 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://github.com/SFDO-Tooling/Snowfakery/schema/snowfakery_recipe.jsonschema.json", + "description": "Snowfakery recipe schema", + "title": "Snowfakery Recipe", + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/$defs/object" + }, + { + "$ref": "#/$defs/include_file" + }, + { + "$ref": "#/$defs/var" + }, + { + "$ref": "#/$defs/macro" + }, + { + "$ref": "#/$defs/plugin" + }, + { + "$ref": "#/$defs/option" + } + ] + }, + "$defs": { + "object": { + "title": "Object Template", + "type": "object", + "additionalProperties": false, + "properties": { + "object": { + "description": "The object name", + "type": "string" + }, + "nickname": { + "type": "string" + }, + "just_once": { + "type": "boolean" + }, + "count": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "object" + } + ] + }, + "fields": { + "type": "object", + "additionalProperties": true + }, + "friends": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/$defs/object" + }, + { + "$ref": "#/$defs/var" + } + ] + } + }, + "include": { + "type": "string" + } + }, + "required": [ + "object" + ] + }, + "include_file": { + "title": "Include File", + "type": "object", + "additionalProperties": false, + "properties": { + "include_file": { + "description": "The file name", + "type": "string" + } + }, + "required": [ + "include_file" + ] + }, + "plugin": { + "title": "Plugin Declaration", + "type": "object", + "additionalProperties": false, + "properties": { + "plugin": { + "description": "The plugin name", + "type": "string" + } + }, + "required": [ + "plugin" + ] + }, + "var": { + "title": "Variable", + "type": "object", + "additionalProperties": false, + "properties": { + "var": { + "description": "The var name", + "type": "string" + }, + "value": { + "description": "The value", + "anyOf": [ + { + "$ref": "#/$defs/object" + }, + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "object" + }, + { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/$defs/object" + }, + { + "$ref": "#/$defs/var" + } + ] + } + } + ] + } + }, + "required": [ + "var", + "value" + ] + }, + "option": { + "title": "Variable", + "type": "object", + "additionalProperties": false, + "properties": { + "option": { + "description": "The var name", + "type": "string" + }, + "default": { + "description": "The value", + "anyOf": [ + { + "$ref": "#/$defs/object" + }, + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "object" + }, + { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/$defs/object" + }, + { + "$ref": "#/$defs/var" + } + ] + } + } + ] + } + }, + "required": [ + "option" + ] + }, + "macro": { + "title": "Macro", + "type": "object", + "additionalProperties": false, + "properties": { + "macro": { + "description": "The object name", + "type": "string" + }, + "include": { + "type": "string" + }, + "fields": { + "type": "object", + "additionalProperties": true + }, + "friends": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/$defs/object" + }, + { + "$ref": "#/$defs/var" + } + ] + } + } + }, + "required": [ + "macro" + ] + } + } +} \ No newline at end of file diff --git a/snowfakery/tools/snowcheck.py b/snowfakery/tools/snowcheck.py new file mode 100644 index 00000000..815f3234 --- /dev/null +++ b/snowfakery/tools/snowcheck.py @@ -0,0 +1,37 @@ +import json +from glob import glob +from pathlib import Path + +import click +import yaml +from jsonschema import validate + + +@click.command() +@click.argument("filespecs", nargs=-1) +def validate_recipe(filespecs): + schemajson = ( + Path(__file__).parent.parent.parent / "schema/snowfakery_recipe.jsonschema.json" + ) + assert schemajson.exists() + + with schemajson.open() as f: + schema = json.load(f) + files = [] + for filespec in filespecs: + files.extend(glob(filespec)) + + if not files: + raise click.ClickException("No files matched!") + + for file in files: + with open(file) as f: + data = yaml.safe_load(f) + try: + validate(instance=data, schema=schema) + except Exception as e: + print(f"ERROR with file {file}: {e}") + + +if __name__ == "__main__": + validate_recipe() diff --git a/tests/errors/empty.yml b/tests/errors/empty.yml new file mode 100644 index 00000000..e69de29b diff --git a/tests/errors/object_error.yml b/tests/errors/object_error.yml new file mode 100644 index 00000000..683ae1ed --- /dev/null +++ b/tests/errors/object_error.yml @@ -0,0 +1,2 @@ +- object: Foo + error: blah diff --git a/tests/errors/top_level_junk.yml b/tests/errors/top_level_junk.yml new file mode 100644 index 00000000..5c3d5b0b --- /dev/null +++ b/tests/errors/top_level_junk.yml @@ -0,0 +1 @@ +- foo: Bar \ No newline at end of file diff --git a/tests/errors/top_level_junk_2.yml b/tests/errors/top_level_junk_2.yml new file mode 100644 index 00000000..13c0c3fe --- /dev/null +++ b/tests/errors/top_level_junk_2.yml @@ -0,0 +1,3 @@ +- object: Account + +- bar: baz \ No newline at end of file diff --git a/tests/test_schema.py b/tests/test_schema.py new file mode 100644 index 00000000..4fd581a9 --- /dev/null +++ b/tests/test_schema.py @@ -0,0 +1,37 @@ +from glob import glob +from jsonschema import validate +from jsonschema.exceptions import ValidationError +import json +import pytest +import yaml + +skippables = ["load.yml", "mapping_"] + + +class TestSchema: + def test_schema(self): + with open("schema/snowfakery_recipe.jsonschema.json") as f: + schema = json.load(f) + files = glob("tests/*.yml") + glob("examples/*.yml") + files = [ + f for f in files if not any(skippable in f for skippable in skippables) + ] + for file in files: + with open(file) as f: + data = yaml.safe_load(f) + print(file) + validate(instance=data, schema=schema) + print("Success", file) + + def test_bad_recipes(self): + with open("schema/snowfakery_recipe.jsonschema.json") as f: + schema = json.load(f) + files = glob("tests/errors/*.yml") + + for file in files: + with open(file) as f: + data = yaml.safe_load(f) + print(file) + with pytest.raises(ValidationError): + validate(instance=data, schema=schema) + print("Success", file)