Sorry about the vague title, but I really don't know how to describe this problem concisely.
I've created a (more or less) simple domain-specific language that I will to use to specify what validation rules to apply to different entities (generally forms submitted from a web page). I've included a sample at the bottom of this post of what the language looks like.
My problem is that I have no idea how to begin parsing this language into a form I can use (I will be using Python to do the parsing). My goal is to end up with a list of rules/filters (as strings, including arguments, e.g. 'cocoa(99)'
) that should be applied (in order) to each object/entity (also a string, e.g. 'chocolate'
, 'chocolate.lindt'
, etc.).
I'm not sure what technique to use to start with, or even what techniques exist for problems like this. What do you think is the best way of going about this? I'm not looking for a complete solution, just a general nudge in the right direction.
Thanks.
Sample file of language:
# Comments start with the '#' character and last until the end of the line
# Indentation is significant (as in Python)
constant NINETY_NINE = 99 # Defines the constant `NINETY_NINE` to have the value `99`
*: # Applies to all data
isYummy # Everything must be yummy
chocolate: # To validate, say `validate("chocolate", object)`
sweet # chocolate must be sweet (but not necessarily chocolate.*)
lindt: # To validate, say `validate("chocolate.lindt", object)`
tasty # Applies only to chocolate.lindt (and not to chocolate.lindt.dark, for e.g.)
*: # Applies to all data under chocolate.lindt
smooth # Could also be written smooth()
creamy(1) # Level 1 creamy
dark: # dark has no special validation rules
extraDark:
melt # Filter that modifies the object being examined
c:bitter # Must be bitter, but only validated on client
s:cocoa(NINETY_NINE) # Must contain 99% cocoa, but only validated on server. Note constant
milk:
creamy(2) # Level 2 creamy, overrides creamy(1) of chocolate.lindt.* for chocolate.lindt.milk
creamy(3) # Overrides creamy(2) of previous line (all but the last specification of a given rule are ignored)
ruleset food: # To define a chunk of validation rules that can be expanded from the placeholder `food` (think macro)
caloriesWithin(10, 2000) # Unlimited parameters allowed
edible
leftovers: # Nested rules allowed in rulesets
stale
# Rulesets may be nested and/or include other rulesets in their definition
chocolate: # Previously defined groups can be re-opened and expanded later
ferrero:
hasHazelnut
cake:
tasty # Same rule used for different data (see chocolate.lindt)
isLie
ruleset food # Substitutes with rules defined for food; cake.leftovers must now be stale
pasta:
ruleset food # pasta.leftovers must also be stale
# Sample use (in JavaScript):
# var choc = {
# lindt: {
# cocoa: {
# percent: 67,
# mass: '27g'
# }
# }
# // Objects/groups that are ommitted (e.g. ferrro in this example) are not validated and raise no errors
# // Objects that are not defined in the validation rules do not raise any errors (e.g. cocoa in this example)
# };
# validate('chocolate', choc);
# `validate` called isYummy(choc), sweet(choc), isYummy(choc.lindt), smooth(choc.lindt), creamy(choc.lindt, 1), and tasty(choc.lindt) in that order
# `validate` returned an array of any validation errors that were found
# Order of rule validation for objects:
# The current object is initially the object passed in to the validation function (second argument).
# The entry point in the rule group hierarchy is given by the first argument to the validation function.
# 1. First all rules that apply to all objects (defined using '*') are applied to the current object,
# starting with the most global rules and ending with the most local ones.
# 2. Then all specific rules for the current object are applied.
# 3. Then a depth-first traversal of the current object is done, repeating steps 1 and 2 with each object found as the current object
# When two rules have equal priority, they are applied in the order they were defined in the file.
# No need to end on blank line
The language you've shown an example for is probably too complex to write a simple (and bug-free) parsing function for. I'd suggest reading up on parsing techniques such as recursive-descent or table-driven parsing such as LL(1), LL(k), etc.
But that may be too general and/or complicated. It might be easier to simplify your rules language to something simple like delimited text.
For example, something like
chocolate:sweet
chocolate.lindt:tasty
chocolate.lindt.*:smooth,creamy(1)
This would be easier to parse and could be done without formal parsers.