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
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()
signature.asc
Description: PGP signature
_______________________________________________ Standards mailing list Info: https://mail.jabber.org/mailman/listinfo/standards Unsubscribe: standards-unsubscr...@xmpp.org _______________________________________________