chiark / gitweb /
service.py: Fix CommandRemoteService handling of vectors.
[chopwood] / service.py
index 786ab92debe05526ef09178059ccd74212a6bcdb..b1f170ca8445e9ce350f4075b8865f0304b558f1 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')