;; ;; brmsaptool.scm ;; ;; ISC License ;; ;; Copyright 2023 Brmlab, z.s. ;; Dominik Pantůček ;; ;; Permission to use, copy, modify, and/or distribute this software ;; for any purpose with or without fee is hereby granted, provided ;; that the above copyright notice and this permission notice appear ;; in all copies. ;; ;; THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL ;; WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED ;; WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE ;; AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR ;; CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS ;; OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, ;; NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN ;; CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ;; (import (chicken condition) (chicken file) (chicken pathname) (chicken file posix) (chicken io) (chicken string) (chicken format) (chicken sort) (chicken time) (chicken time posix) (chicken process-context) testing dictionary month period) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Static default configuration (define *members-directory* (make-parameter "members")) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Testing (define-syntax with-handler (syntax-rules () ((_ handler body ...) (call-with-current-continuation (lambda (k) (with-exception-handler (lambda (x) (k (handler x))) (lambda () body ...))))))) (define-syntax unit-test (syntax-rules () ((_ name condition) (if (with-handler (lambda (x) #f) condition) (display ".") (error 'unit-test name))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Member info data file ;; Member File Parser: remove comments from line and return the result (define (mfp:line:remove-comments l) (let ((si (substring-index "#" l))) (if si (if (= si 0) "" (car (string-chop l si))) l))) ;; Parses given key-value line. Key is up to first space, value is the ;; rest of the line. If the line doesn't contain anything, returns #f. (define (parse-member-line l) (let ((sp (string-split (mfp:line:remove-comments l) " "))) (and sp (not (null? sp)) (list (string->symbol (car sp)) (string-intersperse (cdr sp)))))) ;; If given symbol represents start/stop symbol of either kind, ;; returns a list of the symbol representing the type and start/stop ;; symbol. It returns false otherwise. (define (split-start/stop-symbol s) (cond ((eq? s 'studentstart) '(student start)) ((eq? s 'studentstop) '(student stop)) ((eq? s 'suspendstart) '(suspend start)) ((eq? s 'suspendstop) '(suspend stop)) (else #f))) ;; Processes member line adding given value v to the dictionary d ;; under key k. Special handling for start/stop symbols means given ;; value is prepended to given start/stop key (student/suspend) as ;; parsed month for later processing of student/suspend periods. (define (process-member-line d k v) (let ((ss (split-start/stop-symbol k))) (if ss (let ((pk (car ss)) (pd (cadr ss)) (vl (string-split v " "))) (if (null? vl) (error 'process-member-line "Missing date for start/stop symbol" k) (let ((ds (car vl))) (dict-set d pk (cons (cons pd (string->month ds)) (dict-ref d pk '())))))) (case k ((card desfire credit) (dict-set d k (cons v (dict-ref d k '())))) (else (dict-set d k v)))))) ;; Converts given key in member info dictionary from period markers ;; list to periods. (define (convert-member-key:markers->periods m k) (dict-set m k (period-markers->periods (sort-period-markers (dict-ref m k '()))))) ;; Converts all given keys using period-markers->periods. (define (convert-member-keys:markers->periods m . ks) (let loop ((m m) (ks ks)) (if (null? ks) m (loop (convert-member-key:markers->periods m (car ks)) (cdr ks))))) ;; Fills-in the defaults (define (make-default-member-info) (dict-set (make-dict) 'joined "2015-01")) ;; Processes all lines and returns a dictionary representing given ;; member. (define (parse-member-lines ls) (let loop ((ls ls) (r (make-default-member-info))) (if (null? ls) (convert-member-keys:markers->periods r 'suspend 'student) (let ((p (parse-member-line (car ls)))) (loop (cdr ls) (if p (apply process-member-line r p) r)))))) ;; Loads lines from given file in (*members-directory*) and parses ;; them. (define (load-member-file fn) (let* ((ffn (make-pathname (*members-directory*) fn)) (f (open-input-file ffn)) (ls (read-lines f)) (md (parse-member-lines ls))) (display ".") md)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Members database ;; Loads all symlinks from (*members-directory*) returning a list of ;; pairs (name . destination) (define (load-members-raw-index) (let loop ((fns (directory (*members-directory*))) (rs '())) (if (null? fns) (let () (display "-") rs) (let* ((fn (car fns)) (ffn (make-pathname (*members-directory*) fn)) (sl (if (symbolic-link? ffn) (read-symbolic-link ffn) #f))) (loop (cdr fns) (if sl (cons (cons fn sl) rs) rs)))))) ;; Converts the raw members index to a list of dictionaries with keys ;; 'id, 'name and 'file. File names are without directory element. (define (expand-members-raw-index ri) (let loop ((ri ri) (ds '())) (if (null? ri) (let () (display "*") ds) (let* ((mp (car ri)) (lnk (car mp)) (dfn (cdr mp)) (lnkn (string->number lnk)) (dfnn (string->number dfn)) (id (or dfnn lnkn)) (name (if lnkn dfn lnk))) (loop (cdr ri) (cons (list (cons 'id id) (cons 'name name) (cons 'file dfn)) ds)))))) ;; Adds the 'info key to all expanded index entries by loading ;; appropriate 'file key file from the members directory. (define (load-members-from-expanded-index ei) (let loop ((ei ei) (mdb '())) (if (null? ei) mdb (let ((mi (car ei))) (let ((mid (load-member-file (dict-ref mi 'file)))) (loop (cdr ei) (cons (dict-set mi 'info mid) mdb))))))) ;; Loads all member information from given members database. (define (load-members) (load-members-from-expanded-index (expand-members-raw-index (load-members-raw-index)))) ;; Gets member based by generic predicate (define (find-member-by-predicate mdb pred) (let loop ((mdb mdb)) (if (null? mdb) #f (let ((mr (car mdb))) (if (pred mr) mr (loop (cdr mdb))))))) ;; Gets member record by member key (define (find-member-by-key mdb key val) (find-member-by-predicate mdb (lambda (mr) (equal? (dict-ref mr key) val)))) ;; Gets member record by member id (from file/symlink) (define (find-member-by-id mdb id) (find-member-by-key mdb 'id id)) ;; Gets member record by member name (from file/symlink) (define (find-member-by-name mdb name) (find-member-by-key mdb 'name name)) ;; Gets member record by member nick in member file the key 'nick (define (find-member-by-nick mdb nick) (find-member-by-predicate mdb (lambda (mr) (equal? (dict-ref (dict-ref mr 'info) 'nick) nick)))) ;; Returns the list of all members ids (define (list-members-ids mdb) (map (lambda (mr) (dict-ref mr 'id)) mdb)) ;; Returns the list of all file names in members database (define (list-members-names mdb) (map (lambda (mr) (dict-ref mr 'name)) mdb)) ;; Returns the list of all members nicks (define (list-members-nicks mdb) (map (lambda (mr) (dict-ref (dict-ref mr 'info) 'nick)) mdb)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Member predicates (define (member-suspended? mr) #f) (define (member-student? mr) #f) (define (member-destroyed? mr) #f) ;; Returns true if the member is neither suspended nor destroyed (define (member-active? mr) (not (or (member-suspended? mr) (member-destroyed? mr)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Command-line parsing ;; Prints simple help (define (print-help) (print "Command-line arguments: -h prints this help -M dir specifies the members database directory -mi id member id -mn nick member nick ") (exit 0)) ;; Consumes given number of arguments from the list and returns the ;; remainder of the list and a list of arguments consumed. (define (consume-args args num) (let loop ((args args) (res '()) (num num)) (if (= num 0) (list args (reverse res)) (if (null? args) (error 'consume-args "Not enough arguments" num) (loop (cdr args) (cons (car args) res) (- num 1)))))) ;; Gets command-line arguments after the "--" of csi (not useful when ;; compiled) (define (get-command-line-arguments) (let* ((args (argv)) (rargs (member "--" args))) (if rargs (cdr rargs) (cdr args)))) ;; Performs the actual parsing based on specification. (define (do-parse-command-line specs) (let loop ((args (get-command-line-arguments))) (when (not (null? args)) (let* ((arg (car args)) (specp (assoc arg specs))) (when (not specp) (error 'parse-command-line "Unknown argument" arg)) (let* ((proc (cadr specp)) (info (procedure-information proc)) (nargs (- (length info) 1)) (aargsl (consume-args (cdr args) nargs)) (args (car aargsl)) (aargs (cadr aargsl))) (apply proc aargs) (loop args)))))) ;; Simple syntax wrapper for command-line arguments specification (define-syntax parse-command-line (syntax-rules () ((_ ((arg proc) ...)) (do-parse-command-line `((arg ,proc) ...))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Run everything (define action (make-parameter #f)) (define member-parm (make-parameter #f)) ;; Print banner (print "brmsaptool 0.1 (c) 2023 Brmlab, z.s.") (newline) ;; Handle options (parse-command-line (("-h" print-help) ("-M" (lambda (dn) (*members-directory* dn))) ("-mi" (lambda (id) (action 'member-by-id) (member-parm (string->number id)))) ("-mn" (lambda (nick) (action 'member-by-nick) (member-parm nick))) )) ;; Run tests (print "Running self-tests:") (dictionary-tests!) (month-tests!) (period-tests!) (print "All self-tests ok!") (newline) ;; Load the members database (display "Loading members ") (define MDB (load-members)) (print " ok.") (print "Members in database: " (length MDB)) (newline) ;; Perform requested action (case (action) ((member-by-id member-by-nick) (let ((mr (if (eq? (action) 'member-by-id) (find-member-by-id MDB (member-parm)) (find-member-by-nick MDB (member-parm))))) (if mr (let* ((id (dict-ref mr 'id)) (name (dict-ref mr 'name)) (info (dict-ref mr 'info)) (sinfo (sort info (lambda (a b) (stringstring (car a)) (symbol->string (car b))))))) (print "User " id " alias " name) (let loop ((sinfo sinfo)) (when (not (null? sinfo)) (let* ((kv (car sinfo)) (k (car kv)) (v (cdr kv))) (print " " k ":\t" (if (member k '(student suspend)) (periods->string v) v)) (loop (cdr sinfo)))))) (let () (print "No such member " (member-parm) "."))))) ((#f) (print "No action specified.")))