service.py: Fix CommandRemoteService handling of vectors.
authorMark Wooding <mdw@distorted.org.uk>
Mon, 22 Dec 2014 20:32:58 +0000 (20:32 +0000)
committerMark Wooding <mdw@distorted.org.uk>
Sat, 4 Apr 2015 15:29:16 +0000 (16:29 +0100)
The CommandRemoteService class previously couldn't handle vector
arguments at all, and in particular it dropped the FIELDS argument to
`mkpwent' on the floor.  It also dropped the PASSWORD argument, which
was just stupid.

Convert `_mkcmd' to handle all arguments as vectors, and fix the callers
to wrap their scalar arguments in little vectors.  Now we take the cross
product of all of the arguments when substituting templates.

service.py

index 786ab92..b1f170c 100644 (file)
@@ -348,8 +348,14 @@ class CommandRemoteService (BasicRemoteService):
   containing `%' placeholders, as follows:
 
   `%u'          the user's name
+  `%f'          a user record field (list-valued)
   `%%'          a single `%' character
 
+  If a template word contains placeholders for list-valued arguments, then
+  one output word is produced for each element of each list, with the
+  rightmost placeholder varying fastest.  If any list is empty then no output
+  words are produced.
+
   On success, the commands should print a line `OK' to standard output, and
   on any kind of anticipated failure, they should print `ERR' followed by an
   HTTP status code and a message; in either case, the program should exit
@@ -379,15 +385,60 @@ class CommandRemoteService (BasicRemoteService):
     """Description of the remote service."""
     return "`%s' command service (%s)" % (me.name, ' '.join(me._default))
 
-  def _subst(me, c, map):
-    """Return the substitution for the placeholder `%C'."""
-    return map.get(c, c)
+  def _mkcmd(me, cmd, argmap):
+    """
+    Construct the command to be executed, by substituting placeholders.
+
+    The ARGMAP is a dictionary mapping placeholder letters to lists of
+    arguments.  These are substituted cartesian-product style into the
+    command words.
+    """
 
-  def _mkcmd(me, cmd, map):
-    """Construct the command to be executed, by substituting placeholders."""
-    if map is None: return cmd
-    return [me.R_PAT.sub(lambda m: me._subst(m.group(1), map), arg)
-            for arg in cmd]
+    ## No command map, so assume someone's already done the hard word.
+    if argmap is None: return cmd
+
+    ## Start on building a list of arguments.
+    ww = []
+
+    ## Work through each template argument in turn...
+    for w in cmd:
+
+      ## Firstly, build a list of lists.  We'll then take the cartesian
+      ## product of these, and concatenate each of the results.
+      pc = []
+      last = 0
+      for m in me.R_PAT.finditer(w):
+        start, end = m.start(0), m.end(0)
+        if start > last: pc.append([w[last:start]])
+        ch = m.group(1)
+        if ch == '%':
+          pc.append(['%'])
+        else:
+          try: pc.append(argmap[m.group(1)])
+          except KeyError: raise U.ExpectedError, (
+            500, "Unknown placeholder `%%%s' in command `%s'" % (ch, cmd))
+        last = end
+      if last < len(w): pc.append([w[last:]])
+
+      ## If any of the components is empty then there's nothing to do for
+      ## this word.
+      if not all(pc): continue
+
+      ## Now do all the substitutions.
+      ii = len(pc)*[0]
+      while True:
+        ww.append(''.join(map(lambda v, i: v[i], pc, ii)))
+        i = len(ii) - 1
+        while i >= 0:
+          ii[i] += 1
+          if ii[i] < len(pc[i]): break
+          ii[i] = 0
+          i -= 1
+        else:
+          break
+
+    ## And finally we're done.
+    return ww
 
   def _dispatch(me, func, op, args, input = None):
     """
@@ -395,13 +446,14 @@ class CommandRemoteService (BasicRemoteService):
 
     Invoke FUNC, which works like `_run', with appropriate arguments.  The OP
     is a remote command name; ARGS is a sequence of (C, ARG) pairs, where C
-    is a placeholder character and ARG is a string value; INPUT is the text
-    to provide to the command on standard input.
+    is a placeholder character and ARG is a list of string values; INPUT is
+    the text to provide to the command on standard input.
     """
     try:
       cmd = me._opmap[op]
     except KeyError:
-      cmd = me._default + [op] + [v for k, v in args]
+      cmd = me._default + [op] + reduce(lambda x, y: x + y,
+                                        [v for k, v in args], [])
       map = None
     else:
       map = dict(args)
@@ -409,19 +461,21 @@ class CommandRemoteService (BasicRemoteService):
 
   def setpasswd(me, user, passwd):
     """Service protocol: set the USER's password to PASSWD."""
-    me._dispatch(me._run_noout, 'set', [('u', user)], passwd + '\n')
+    me._dispatch(me._run_noout, 'set', [('u', [user])],
+                 input = passwd + '\n')
 
   def clearpasswd(me, user):
     """Service protocol: clear the USER's password."""
-    me._dispatch(me._run_noout, 'clear', [('u', user)])
+    me._dispatch(me._run_noout, 'clear', [('u', [user])])
 
   def mkpwent(me, user, passwd, fields):
     """Service protocol: create a record for USER."""
-    me._dispatch(me._run_noout, 'mkpwent', [('u', user)])
+    me._dispatch(me._run_noout, 'mkpwent', [('u', [user]), ('f', fields)],
+                 input = passwd + '\n')
 
   def rmpwent(me, user):
     """Service protocol: delete the record for USER."""
-    me._dispatch(me._run_noout, 'rmpwent', [('u', user)])
+    me._dispatch(me._run_noout, 'rmpwent', [('u', [user])])
 
 CONF.export('CommandRemoteService')