Hi,

For quite a long time I’ve been trying to design a format that would
describe XMPP software, while being useful to most (all?) consumers of
this document, be it simple client listings like at xmpp.org, wikis
about projects wanting to stay up to date, feature matrices comparing
multiple implementations, or even authors who’d like to know who
implemented their specifications.

Attached are an example of an XMPP software (poezio, a client), and of
a consumer (xmpp.org, a simple website).

The format itself still has a lot of TODOs, most of which have been
reported to the DOAP project[1], while the XMPP specific parts still
lack a schema entirely.

The consumer Python script parses the file quite defensively, and then
generates the HTML you could have seen on xmpp.org (for the clients
page[1] and for a XEP page[2]).  The XEP page would require some
changes to the way XEPs are processed, since it generates the change
log itself, inserting implementations there.  Another way would be to
generate JavaScript code that would insert the DOM nodes at runtime.

There is also still the issue of having a place where all of these DOAP
URIs would be linked from, I think xmpp.org could act as such even for
projects unlisted from the main pages, but this remains to be decided.

Please tell me whether you think it is a good idea, whether you would
integrate it with your website/wiki/feature matrix/other, and any
improvement you could think of!

Thanks,

[1] https://github.com/ewilderj/doap/issues
[2] https://xmpp.org/software/clients.html
[3] https://xmpp.org/extensions/xep-0380.html

-- 
Emmanuel Gil Peyrot

Attachment: poezio.xml
Description: XML document

#!/usr/bin/env python

from argparse import ArgumentParser
from datetime import datetime
from enum import Enum
from xml.etree.ElementTree import Element
import xml.etree.ElementTree as ET
import re

NUMBER_RE = re.compile(r'\d\d\d\d')
RFC_RE = re.compile(r'https://xmpp.org/rfcs/rfc(\d\d\d\d).html')
Type = Enum('Type', ['client', 'server', 'component', 'library'])
Status = Enum('Status', ['complete', 'partial', 'started', 'interested'])

TABLE_TEMPLATE = '''
<table>
  <thead>
    <th>Project Name</th>
    <th>Platforms</th>
  <thead>
  <tbody>
%s
  </tbody>
</table>
'''

def parse_args():
    parser = ArgumentParser(description='DoaP parser, with XMPP stuff added')
    parser.add_argument('files', nargs='+', metavar='FILE', type=open, help='The DoaP file(s) to parse')
    return parser.parse_args()

class DoapParseError(Exception):
    pass

class Extension:
    #__slots__ = ('_xml', 'number', 'status', 'version', 'since', 'note')

    def __init__(self, xml: Element):
        self._xml = xml
        self.number = self.parse_number()
        self.note = self.parse_note()
        self.since = self.parse_since()
        self.status = self.parse_status()
        self.version = self.parse_version()

    def __repr__(self):
        return 'XEP-%s (%s, version %s, since %s%s)' % (
            self.number,
            self.status.name,
            self.version,
            self.since if self.since is not None else 'forever',
            ', ' + self.note if self.note is not None else '')

    def parse_number(self):
        number = self._xml.get('number')
        if number is None or not NUMBER_RE.match(number):
            raise DoapParseError(f'invalid number {number} for extension.')
        return number

    def parse_status(self):
        try:
            return Status[self._xml.get('status')]
        except KeyError:
            raise DoapParseError('invalid status for XEP-{self.number}.')

    def parse_version(self):
        version = self._xml.get('version')
        if version is None:
            raise DoapParseError(f'invalid version for XEP-{self.number}.')
        return version

    def parse_since(self):
        return self._xml.get('since')

    def parse_note(self):
        return self._xml.get('note')

class Doap:
    ALLOWED_OS = ('Android', 'Browser', 'iOS', 'Linux', 'macOS', 'Windows')

    #__slots__ = ('_xml', 'name', 'created', 'homepage', 'os', 'xmpp')

    def __init__(self, xml: Element):
        self._xml = xml
        self.name = self.parse_name()
        self.created = self.parse_created()
        self.homepage = self.parse_homepage()
        self.os = self.parse_os()
        self.xmpp = self.parse_xmpp()

    def parse_name(self):
        name = None
        try:
            name = self._xml.find('{http://usefulinc.com/ns/doap#}name').text
        except AttributeError:
            print('Warning: no name defined for project.')
        else:
            if name is None:
                print('Warning: no name defined for project.')
        return name

    def parse_created(self):
        created = None
        try:
            created = datetime.strptime(self._xml.find('{http://usefulinc.com/ns/doap#}created').text, '%Y-%m-%d')
        except ValueError:
            print('Warning: invalid creation date for project.')
        except TypeError:
            print('Warning: invalid creation date for project.')
        except AttributeError:
            print('Warning: missing creation date for project.')
        return created

    def parse_homepage(self):
        homepage = None
        try:
            homepage = self._xml.find('{http://usefulinc.com/ns/doap#}homepage').get('{http://www.w3.org/1999/02/22-rdf-syntax-ns#}resource')
        except AttributeError:
            print('Warning: no homepage defined for project.')
        else:
            if homepage is None:
                print('Warning: no homepage defined for project.')
        return homepage

    def parse_os(self):
        os = {os.text if os.text in self.ALLOWED_OS else 'Other' for os in self._xml.findall('{http://usefulinc.com/ns/doap#}os') if os.text is not None}
        if not os:
            print('Warning: no os defined for project.')
            os = {'Unknown'}
        os = list(os)
        os.sort(key=lambda x: x == 'Other')
        return os

    @staticmethod
    def parse_rfc(xml: Element):
        url = xml.get('{http://www.w3.org/1999/02/22-rdf-syntax-ns#}resource')
        matches = RFC_RE.match(url)
        if matches is None:
            return None
        return matches[1]

    def parse_xmpp(self):
        xmpp = self._xml.find('{xmpp:linkmauve.fr/protocols/xmpp-software}xmpp-software')
        if xmpp is None:
            print('Warning: not an XMPP project.')
            return {}

        software_type = None
        try:
            software_type = Type[xmpp.find('{xmpp:linkmauve.fr/protocols/xmpp-software}type').text]
        except KeyError:
            print('Warning: invalid XMPP type for project.')
        except AttributeError:
            print('Warning: missing XMPP type for project.')

        rfcs = [rfc for rfc in [self.parse_rfc(rfc) for rfc in self._xml.findall('{http://usefulinc.com/ns/doap#}implements')] if rfc is not None]
        extensions = [Extension(ext) for ext in xmpp.findall('{xmpp:linkmauve.fr/protocols/xmpp-software}extension')]

        return {
            'type': software_type,
            'rfcs': rfcs,
            'extensions': {ext.number: ext for ext in extensions},
        }

def generate_table(table: list):
    data = []
    for software in table:
        data.append('''    <tr>
      <td><a href="%s">%s</a></td>
      <td>%s</td>
    </tr>''' % (software.homepage, software.name, ' / '.join(software.os)))
    return TABLE_TEMPLATE % ''.join(data)

def generate_fake_xep_0380(implementers: list):
    FAKE_DATA = [
        ('0.1', '2016-10-26', 'fs', '<p>Initial published version approved by the XMPP Council.</p>'),
        ('0.0.2', '2016-08-28', 'egp', '''<ul>
    <li>Made the 'name' attribute optional for existing mechanisms.</li>
    <li>Added a remark about the possibility to hide encrypted messages
      following user input.</li>
    <li>Made explicit that this protocol affects any encryption mechanism,
      present or future, not only those listed here.</li>
    <li>Display the namespace of the encryption mechanism in the default
      messages.</li>
    </ul>'''),
        ('0.0.1', '2016-08-14', 'egp', '<p>First draft.</p>')
    ]
    xep_data = [(impl, impl.xmpp['extensions']['0380']) for impl in implementers]
    data = []
    for x in FAKE_DATA:
        implementations = ['<a href="%s">%s (%s, since %s)</a>' % (impl.homepage, impl.name, xep.status.name, xep.since) for impl, xep in xep_data if xep.version == x[0]]
        data.append('<h4>Version %s (%s)</h4><p>%s</p><div class="indent">%s (%s)</div>' % (x[0], x[1], ' '.join(implementations), x[3], x[2]))
    return '\n'.join(data)

CLIENTS = []
SERVERS = []
COMPONENTS = []
LIBRARIES = []
RFCS = {}
XEPS = {}

def main():
    args = parse_args()
    matches = {
        Type.client: CLIENTS,
        Type.server: SERVERS,
        Type.component: COMPONENTS,
        Type.library: LIBRARIES
    }
    for doap_file in args.files:
        data: str = doap_file.read()
        xml: Element = ET.fromstring(data)
        software: Doap = Doap(xml)

        # Fill the main lists.
        matches[software.xmpp['type']].append(software)
        for rfc in software.xmpp['rfcs']:
            RFCS.setdefault(rfc, []).append(software)
        for ext in software.xmpp['extensions']:
            XEPS.setdefault(ext, []).append(software)

    print(generate_table(CLIENTS))
    #print(generate_table(SERVERS))
    #print(generate_table(LIBRARIES))
    print(generate_fake_xep_0380(XEPS['0380']))

if __name__ == '__main__':
    main()

Attachment: signature.asc
Description: PGP signature

_______________________________________________
Standards mailing list
Info: https://mail.jabber.org/mailman/listinfo/standards
Unsubscribe: standards-unsubscr...@xmpp.org
_______________________________________________

Reply via email to