refactor DomainnameOption
authorEmmanuel Garette <egarette@cadoles.com>
Mon, 30 Sep 2013 17:41:56 +0000 (19:41 +0200)
committerEmmanuel Garette <egarette@cadoles.com>
Mon, 30 Sep 2013 17:41:56 +0000 (19:41 +0200)
add options EmailOption and URLOption

test/test_config_domain.py
tiramisu/option.py

index da04235..9c5c16f 100644 (file)
@@ -2,7 +2,7 @@ import autopath
 from py.test import raises
 
 from tiramisu.config import Config
-from tiramisu.option import DomainnameOption, OptionDescription
+from tiramisu.option import DomainnameOption, EmailOption, URLOption, OptionDescription
 
 
 def test_domainname():
@@ -14,11 +14,12 @@ def test_domainname():
     c.d = 'toto.com'
     raises(ValueError, "c.d = 'toto'")
     c.d = 'toto3.com'
-    c.d = 'toto3.3la'
+    raises(ValueError, "c.d = 'toto3.3la'")
     raises(ValueError, "c.d = '3toto.com'")
-    c.d = 'toto.co3'
+    raises(ValueError, "c.d = 'toto.co3'")
     raises(ValueError, "c.d = 'toto_super.com'")
     c.d = 'toto-.com'
+    raises(ValueError, "c.d = 'toto..com'")
 
 
 def test_domainname_netbios():
@@ -41,3 +42,30 @@ def test_domainname_hostname():
     raises(ValueError, "c.d = 'toto.com'")
     c.d = 'toto'
     c.d = 'domainnametoolong'
+
+
+def test_email():
+    e = EmailOption('e', '')
+    od = OptionDescription('a', '', [e])
+    c = Config(od)
+    c.read_write()
+    c.e = 'root@foo.com'
+    raises(ValueError, "c.e = 'root'")
+    raises(ValueError, "c.e = 'root@domain'")
+
+
+def test_url():
+    u = URLOption('u', '')
+    od = OptionDescription('a', '', [u])
+    c = Config(od)
+    c.read_write()
+    c.u = 'http://foo.com'
+    c.u = 'https://foo.com'
+    c.u = 'https://foo.com/'
+    raises(ValueError, "c.u = 'ftp://foo.com'")
+    c.u = 'https://foo.com/index.html'
+    c.u = 'https://foo.com/index.html?var=value&var2=val2'
+    raises(ValueError, "c.u = 'https://foo.com/index\\n.html'")
+    c.u = 'https://foo.com:8443'
+    c.u = 'https://foo.com:8443/'
+    c.u = 'https://foo.com:8443/index.html'
index c7a28c2..dec7b02 100644 (file)
@@ -968,20 +968,35 @@ class DomainnameOption(Option):
     domainname:
     fqdn: with tld, not supported yet
     """
-    __slots__ = ('_type', '_allow_ip')
+    __slots__ = ('_type', '_allow_ip', '_allow_without_dot', '_domain_re')
     _opt_type = 'domainname'
 
     def __init__(self, name, doc, default=None, default_multi=None,
                  requires=None, multi=False, callback=None,
                  callback_params=None, validator=None, validator_params=None,
                  properties=None, allow_ip=False, type_='domainname',
-                 warnings_only=False):
+                 warnings_only=False, allow_without_dot=False):
         if type_ not in ['netbios', 'hostname', 'domainname']:
             raise ValueError(_('unknown type_ {0} for hostname').format(type_))
         self._type = type_
         if allow_ip not in [True, False]:
             raise ValueError(_('allow_ip must be a boolean'))
+        if allow_without_dot not in [True, False]:
+            raise ValueError(_('allow_without_dot must be a boolean'))
         self._allow_ip = allow_ip
+        self._allow_without_dot = allow_without_dot
+        end = ''
+        extrachar = ''
+        if self._type == 'netbios':
+            length = 14
+        elif self._type == 'hostname':
+            length = 62
+        elif self._type == 'domainname':
+            length = 62
+            extrachar = '\.'
+            end = '+[a-z]*'
+        self._domain_re = re.compile(r'^(?:[a-z][a-z\d\-]{{,{0}}}{1}){2}$'
+                                     ''.format(length, extrachar, end))
         super(DomainnameOption, self).__init__(name, doc, default=default,
                                                default_multi=default_multi,
                                                callback=callback,
@@ -1000,27 +1015,82 @@ class DomainnameOption(Option):
                 return
             except ValueError:
                 pass
-        if self._type == 'netbios':
-            length = 15
-            extrachar = ''
-        elif self._type == 'hostname':
-            length = 63
-            extrachar = ''
-        elif self._type == 'domainname':
-            length = 255
-            extrachar = '\.'
-            if '.' not in value:
-                raise ValueError(_("invalid value for {0}, must have dot"
-                                   "").format(self._name))
-        if len(value) > length:
-            raise ValueError(_("invalid domainname's length for"
-                               " {0} (max {1})").format(self._name, length))
-        if len(value) == 1:
+        if self._type == 'domainname' and not self._allow_without_dot and \
+                '.' not in value:
+            raise ValueError(_("invalid domainname for {0}, must have dot"
+                               "").format(self._name))
+            if len(value) > 255:
+                raise ValueError(_("invalid domainname's length for"
+                                   " {0} (max 255)").format(self._name))
+        if len(value) < 2:
             raise ValueError(_("invalid domainname's length for {0} (min 2)"
                                "").format(self._name))
-        regexp = r'^[a-z]([a-z\d{0}-])*[a-z\d]$'.format(extrachar)
-        if re.match(regexp, value) is None:
-            raise ValueError(_('invalid domainname'))
+        if not self._domain_re.search(value):
+            raise ValueError(_('invalid domainname: {0}'.format(self._name)))
+
+
+class EmailOption(DomainnameOption):
+    __slots__ = tuple()
+    username_re = re.compile(r"^[\w!#$%&'*+\-/=?^`{|}~.]+$")
+
+    def __init__(self, *args, **kwargs):
+        kwargs['type_'] = 'domainname'
+        kwargs['allow_ip'] = False
+        kwargs['allow_without_dot'] = False
+        super(EmailOption, self).__init__(*args, **kwargs)
+
+    def _validate(self, value):
+        splitted = value.split('@', 1)
+        try:
+            username, domain = splitted
+        except ValueError:
+            raise ValueError(_('invalid email address, should contains one @ '
+                               'for {0}').format(self._name))
+        if not self.username_re.search(username):
+            raise ValueError(_('invalid username in email address for {0}').format(self._name))
+        super(EmailOption, self)._validate(domain)
+
+
+class URLOption(DomainnameOption):
+    __slots__ = tuple()
+    proto_re = re.compile(r'(http|https)://')
+    path_re = re.compile(r"^[a-z0-9\-\._~:/\?#\[\]@!%\$&\'\(\)\*\+,;=]+$")
+
+    def __init__(self, *args, **kwargs):
+        kwargs['type_'] = 'domainname'
+        kwargs['allow_ip'] = False
+        kwargs['allow_without_dot'] = False
+        super(URLOption, self).__init__(*args, **kwargs)
+
+    def _validate(self, value):
+        match = self.proto_re.search(value)
+        if not match:
+            raise ValueError(_('invalid url, should start with http:// or '
+                               'https:// for {0}').format(self._name))
+        value = value[len(match.group(0)):]
+        # get domain/files
+        splitted = value.split('/', 1)
+        try:
+            domain, files = splitted
+        except ValueError:
+            domain = value
+            files = None
+        # if port in domain
+        splitted = domain.split(':', 1)
+        try:
+            domain, port = splitted
+
+        except ValueError:
+            domain = splitted[0]
+            port = 0
+        if not 0 <= int(port) <= 65535:
+            raise ValueError(_('port must be an between 0 and 65536'))
+        # validate domainname
+        super(URLOption, self)._validate(domain)
+        # validate file
+        if files is not None and files != '' and not self.path_re.search(files):
+            raise ValueError(_('invalid url, should endswith with filename for'
+                               ' {0}').format(self._name))
 
 
 class OptionDescription(BaseOption):