spec-ast.rkt
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; spec-ast.rkt
;; Richard Cobbe
;; January 2011
;;
;; This module defines the abstract syntax representation of a command line
;; specification.
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

#lang racket

;; We can't use typed racket for this module, as it's too hard to write down
;; the type for the function field in command.

(require (prefix-in CU: (planet cobbe/contract-utils:4/contract-utils)))

(struct spec (program help commands) #:transparent)
(struct command (name short-help long-help positional-args
                      rest-arg flags function) #:transparent)
(struct flag () #:transparent)
(struct optional-flag flag (switches keyword help args) #:transparent)
(struct multi-flag flag (switches keyword help args) #:transparent)
(struct group flag (flags) #:transparent)

;; Invariants:
;;   - within a command, all keywords and switches are unique
;;   - each multi-flag must have at least one arg
;;   - order of flags matches order in spec (for ease of testing &
;;     control over order of help text)
;;   - a group must have at least two flags.
;;   - Within a flag, duplicate switches are silently ignored.
;;   - Duplicate flag args are acceptable, as these are matched positionally;
;;     we only use the name for --help documentation.

(provide/contract
 (struct spec ([program string?]
               [help string?]
               [commands (CU:nelistof/c command?)]))
 (struct command ([name string?]
                  [short-help string?]
                  [long-help string?]
                  [positional-args (listof symbol?)]
                  [rest-arg (CU:optional/c symbol?)]
                  [flags (listof flag?)]
                  [function procedure?]))
 (struct (optional-flag flag) ([switches set?] ;; set of switch
                               [keyword keyword?]
                               [help (CU:nelistof/c string?)]
                               [args (listof symbol?)]))
 (struct (multi-flag flag) ([switches set?] ;; set of switch
                            [keyword keyword?]
                            [help (CU:nelistof/c string?)]
                            [args (CU:nelistof/c symbol?)]))
 (struct (group flag) ([flags (CU:nelistof/c optional-flag?)]))

 [switch? (string? . -> . boolean?)]
 [short-switch? (string? . -> . boolean?)]
 [long-switch? (string? . -> . boolean?)]

 [find-command (spec? string? . -> . (CU:optional/c command?))])

;; Recognizes strings that are valid switches, like -x, --foo, --help
(define switch?
  (lambda (x)
    (or (short-switch? x) (long-switch? x))))

;; short-switch? :: String -> Boolean
;; recognizes short switches
(define short-switch?
  (lambda (str)
    (and (regexp-match #rx"^-[A-Za-z]$" str) #t)))

;; long-switch? :: String -> Boolean
;; recognizes long switches
(define long-switch?
  (lambda (str)
    (and (regexp-match #rx"^--[A-Za-z0-9][-A-Za-z0-9]*$" str) #t)))

;; Finds a command by name; returns #f if no match
(define find-command
  (lambda (spec cmd-name)
    (findf (lambda (c) (string=? cmd-name (command-name c)))
           (spec-commands spec))))