descriptive ===== Self-describing consumers/parsers [Haddocks](http://chrisdone.com/descriptive/) There are a variety of Haskell libraries which are implementable through a common interface: self-describing parsers: * A formlet is a self-describing parser. * A regular old text parser can be self-describing. * A command-line options parser is a self-describing parser. * A MUD command set is a self-describing parser. * A JSON API can be a self-describing parser. Consumption is done in this data type: ``` haskell data Consumer s d a ``` To make a consumer, this combinator is used: ``` haskell consumer :: (s -> (Description d,s)) -> (s -> (Either (Description d) a,s)) -> Consumer s d a ``` The first argument generates a description based on some state. The state is determined by whatever use-case you have. The second argument parses from the state, which could be a stream of bytes, a list of strings, a Map, a Vector, etc. You may or may not decide to modify the state during generation of the description and during parsing. To use a consumer or describe what it does, these are used: ``` haskell consume :: Consumer s d a -> s -> (Either (Description d) a,s) describe :: Consumer s d a -> s -> (Description d,s) ``` A description is like this: ``` haskell data Description a = Unit !a | Bounded !Integer !Bound !(Description a) | And !(Description a) !(Description a) | Sequence [Description a] | Wrap a (Description a) | None deriving (Show) ``` You configure the `a` for your use-case, but the rest is generatable by the library. Afterwards, you can make your own pretty printing function, which may be to generate an HTML form, to generate a commandline `--help` screen, a man page, API docs for your JSON parser, a text parsing grammar, etc. For example: ``` haskell describeParser :: Consumer [Char] Text Demo -> Text describeForm :: Consumer (Map Text Text) (Html ()) Login -> Html () describeArgs :: Consumer [Text] CmdArgs MyApp -> CmdArgs ``` See below for some examples of this library. ## Parsing characters See `Descriptive.Char`. ``` haskell λ> describe (many (char 'k') <> string "abc") mempty (And (Bounded 0 UnlimitedBound (Unit "k")) (Sequence [Unit "a",Sequence [Unit "b",Sequence [Unit "c",Sequence []]]]) ,"") λ> consume (many (char 'k') <> string "abc") "kkkabc" (Right "kkkabc","") λ> consume (many (char 'k') <> string "abc") "kkkab" (Left (Unit "a character"),"") λ> consume (many (char 'k') <> string "abc") "kkkabj" (Left (Unit "c"),"") ``` ## Validating forms with named inputs See `Descriptive.Form`. ``` haskell λ> describe ((,) <$> input "username" <*> input "password") mempty (And (Unit (Input "username")) (Unit (Input "password")),fromList []) λ> consume ((,) <$> input "username" <*> input "password") (M.fromList [("username","chrisdone"),("password","god")]) (Right ("chrisdone","god") ,fromList [("password","god"),("username","chrisdone")]) ``` Conditions on two inputs: ``` haskell login = validate "confirmed password (entered the same twice)" (\(x,y) -> if x == y then Just y else Nothing) ((,) <$> input "password" <*> input "password2") ``` ``` haskell λ> describe login mempty (Wrap (Constraint "confirmed password (entered the same twice)") (And (Unit (Input "password")) (Unit (Input "password2"))) ,fromList []) λ> consume login (M.fromList [("password2","gob"),("password","gob")]) (Right "gob",fromList [("password","gob"),("password2","gob")]) ``` ## Validating forms with auto-generated input indexes See `Descriptive.Formlet`. ``` haskell λ> describe ((,) <$> indexed <*> indexed) (FormletState mempty 0) (And (Unit (Index 0)) (Unit (Index 1)) ,FormletState {formletMap = fromList [] ,formletIndex = 2}) λ> consume ((,) <$> indexed <*> indexed) (FormletState (M.fromList [(0,"chrisdone"),(1,"god")]) 0) (Right ("chrisdone","god") ,FormletState {formletMap = fromList [(0,"chrisdone"),(1,"god")] ,formletIndex = 2}) λ> consume ((,) <$> indexed <*> indexed) (FormletState (M.fromList [(0,"chrisdone")]) 0) (Left (Unit (Index 1)) ,FormletState {formletMap = fromList [(0,"chrisdone")] ,formletIndex = 2}) ``` ## Parsing command-line options See `Descriptive.Options`. ``` haskell server = ((,,,) <$> constant "start" <*> anyString "SERVER_NAME" <*> flag "dev" "Enable dev mode?" <*> arg "port" "Port to listen on") ``` ``` haskell λ> describe server [] (And (And (And (Unit (Constant "start")) (Unit (AnyString "SERVER_NAME"))) (Unit (Flag "dev" "Enable dev mode?"))) (Unit (Arg "port" "Port to listen on")) ,[]) λ> consume server ["start","any","--port","1234","-fdev"] (Right ("start","any",True,"1234"),[]) λ> consume server ["start","any","--port","1234"] (Right ("start","any",False,"1234"),[]) λ> consume server ["start","any"] (Left (Unit (Arg "port" "Port to listen on")),[]) ``` ## Self-documenting JSON parser See `Descriptive.JSON`. ``` haskell -- | Submit a URL to reddit. data Submission = Submission {submissionToken :: !Integer ,submissionTitle :: !Text ,submissionComment :: !Text ,submissionSubreddit :: !Integer} deriving (Show) submission :: Consumer Value Doc Submission submission = obj "Submission" (Submission <$> key "token" (integer "Submission token; see the API docs") <*> key "title" (string "Submission title") <*> key "comment" (string "Submission comment") <*> key "subreddit" (integer "The ID of the subreddit")) sample :: Value sample = toJSON (object ["token" .= 123 ,"title" .= "Some title" ,"comment" .= "This is good" ,"subreddit" .= 234214]) badsample :: Value badsample = toJSON (object ["token" .= 123 ,"title" .= "Some title" ,"comment" .= 123 ,"subreddit" .= 234214]) ``` ``` haskell λ> describe submission (toJSON ()) (Wrap (Struct "Submission") (And (And (And (Wrap (Key "token") (Unit (Integer "Submission token; see the API docs"))) (Wrap (Key "title") (Unit (Text "Submission title")))) (Wrap (Key "comment") (Unit (Text "Submission comment")))) (Wrap (Key "subreddit") (Unit (Integer "The ID of the subreddit")))) ,Array (fromList [])) λ> consume submission sample (Right (Submission {submissionToken = 123 ,submissionTitle = "Some title" ,submissionComment = "This is good" ,submissionSubreddit = 234214}) ,Object (fromList [("token",Number 123.0) ,("subreddit",Number 234214.0) ,("title",String "Some title") ,("comment",String "This is good")])) λ> consume submission badsample (Left (Wrap (Struct "Submission") (Wrap (Key "comment") (Unit (Text "Submission comment")))) ,Object (fromList [("token",Number 123.0) ,("subreddit",Number 234214.0) ,("title",String "Some title") ,("comment",Number 123.0)])) ``` The bad sample yields an informative message that: * The error is in the Submission object. * The key "comment". * The type of that key should be a String and it should be a Submission comment (or whatever invariants you'd like to mention). ## Parsing Attempto Controlled English for MUD commands TBA. Will use [this package](http://chrisdone.com/posts/attempto-controlled-english). With ACE you can parse into: ``` haskell parsed complV " a a " == Right (ComplVDisV (DistransitiveV "") (ComplNP (NPCoordUnmarked (UnmarkedNPCoord anoun Nothing))) (ComplPP (PP (Preposition "") (NPCoordUnmarked (UnmarkedNPCoord anoun Nothing))))) ``` Which I can then further parse with `descriptive` to yield descriptions like: [ ..] Or similar. Which would be handy for a MUD so that a user can write: > Put the sword on the table. ## Producing questions and consuming the answers in Haskell TBA. Will be a generalization of [this type](https://github.com/chrisdone/exercise/blob/master/src/Exercise/Types.hs#L20). It is a library which I am working on in parallel which will ask the user questions and then validate the answers. Current output is like this: ``` haskell λ> describe (greaterThan 4 (integerExpr (parse id expr exercise))) an integer greater than 4 λ> eval (greaterThan 4 (integerExpr (parse id expr exercise))) $(someHaskell "x = 1") Left expected an expression, but got a declaration λ> eval (greaterThan 4 (integerExpr (parse id expr exercise))) $(someHaskell "x") Left expected an integer, but got an expression λ> eval (greaterThan 4 (integerExpr (parse id expr exercise))) $(someHaskell "3") Left expected an integer greater than 4 λ> eval (greaterThan 4 (integerExpr (parse id expr exercise))) $(someHaskell "5") Right 5 ``` This is also couples description with validation, but I will probably rewrite it with this `descriptive` library.