The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.
# -*- coding: utf-8 -*-
"""
    MongoDB Domain for Sphinx
    ~~~~~~~~~~~~~~~~~~~~~~~~~

    Based on the default JavaScript domain distributed with Sphinx.

    :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS.
    :license: BSD, see LICENSE for details.

    Additional work to adapt for MongoDB purposes done by 10gen,
    inc. (Sam Kleinman, et al.)
"""

from sphinx import addnodes
from sphinx.domains import Domain, ObjType
from sphinx.locale import l_, _
from sphinx.directives import ObjectDescription
from sphinx.roles import XRefRole
from sphinx.domains.python import _pseudo_parse_arglist
from sphinx.util.nodes import make_refnode
from sphinx.util.docfields import Field, GroupedField, TypedField

from sphinx_conf import composite_pages

class MongoDBObject(ObjectDescription):
    """
    Description of a MongoDB object.
    """
    #: If set to ``True`` this object is callable and a `desc_parameterlist` is
    #: added
    has_arguments = False

    #: what is displayed right before the documentation entry
    display_prefix = None

    def handle_signature(self, sig, signode):
        sig = sig.strip()
        if '(' in sig and sig[-1:] == ')':
            prefix, arglist = sig.split('(', 1)
            prefix = prefix.strip()
            arglist = arglist[:-1].strip()
        else:
            prefix = sig
            arglist = None
        if '.' in prefix:
            nameprefix, name = prefix.rsplit('.', 1)
        else:
            nameprefix = None
            name = prefix

        objectname = self.env.temp_data.get('mongodb:object')
        if nameprefix:
            if objectname:
                # someone documenting the method of an attribute of the current
                # object? shouldn't happen but who knows...
                nameprefix = objectname + '.' + nameprefix
            fullname = nameprefix + '.' + name
        elif objectname:
            fullname = objectname + '.' + name
        else:
            # just a function or constructor
            objectname = ''
            fullname = name

        signode['object'] = objectname
        signode['fullname'] = fullname

        if self.display_prefix:
            signode += addnodes.desc_annotation(self.display_prefix,
                                                self.display_prefix)
        if nameprefix:
            signode += addnodes.desc_addname(nameprefix + '.', nameprefix + '.')
        signode += addnodes.desc_name(name, name)
        if self.has_arguments:
            if not arglist:
                signode += addnodes.desc_parameterlist()
            else:
                _pseudo_parse_arglist(signode, arglist)
        return fullname, nameprefix

    def add_target_and_index(self, name_obj, sig, signode):
        objectname = self.options.get(
            'object', self.env.temp_data.get('mongodb:object'))

        if self.objtype == 'binary':
            fullname = 'bin.' + name_obj[0]
        else:
            fullname = name_obj[0]

        if fullname not in self.state.document.ids:
            signode['names'].append(fullname)
            signode['ids'].append(fullname.replace('$', '_S_'))
            signode['first'] = not self.names
            self.state.document.note_explicit_target(signode)
            objects = self.env.domaindata['mongodb']['objects']
            if fullname in objects:
                path = self.env.doc2path(self.env.domaindata['mongodb']['objects'][fullname][0])
                spath = path.split('/')[-1].rsplit('.', 1)[0]

                if spath in composite_pages:
                    pass
                elif spath == fullname:
                    pass
                elif spath == fullname.lstrip('$'):
                    pass
                elif spath == fullname.lstrip('_'):
                    pass
                else:
                    self.state_machine.reporter.warning(
                        'duplicate object description of %s, ' % fullname +
                        'other instance in ' + path,
                        line=self.lineno)

            if self.env.docname.rsplit('/', 1)[1] in composite_pages:
                pass
            else:
                objects[fullname] = self.env.docname, self.objtype

        indextext = self.get_index_text(objectname, name_obj)
        if indextext:
            self.indexnode['entries'].append(('single', indextext,
                                              fullname.replace('$', '_S_'),
                                              ''))

    def get_index_text(self, objectname, name_obj):
        name, obj = name_obj
        if self.objtype == 'dbcommand':
            return _('%s (database command)') % name
        elif self.objtype == 'operator':
            return _('%s (operator)') % name
        elif self.objtype == 'projection':
            return _('%s (projection operator)') % name
        elif self.objtype == 'binary':
            return _('%s (program)') % name
        elif self.objtype == 'setting':
            return _('%s (setting)') % (name)
        elif self.objtype == 'status':
            return _('%s (status)') % (name)
        elif self.objtype == 'stats':
            return _('%s (statistic)') % (name)
        elif self.objtype == 'data':
            return _('%s (shell output)') % (name)
        elif self.objtype == 'method':
            return _('%s (shell method)') % (name)
        elif self.objtype == 'collflag':
            return _('%s (collection flag)') % (name)
        elif self.objtype == 'readmode':
            return _('%s (read preference mode)') % (name)
        elif self.objtype == 'error':
            return _('%s (error code)') % (name)
        elif self.objtype == 'macro':
            return _('%s (JavaScript shell macro)') % (name)
        elif self.objtype == 'limit':
            return _('%s (MongoDB system limit)') % (name)
        elif self.objtype == 'bsontype':
            return _('%s (BSON type)') % (name)
        return ''

    def run(self):
        return super(MongoDBObject, self).run()

    doc_field_types = [
        TypedField('arguments', label=l_('Arguments'),
                   names=('argument', 'arg'),
                   typerolename='method', typenames=('paramtype', 'type')),
        TypedField('options', label=l_('Options'),
                   names=('options', 'opts', 'option', 'opt'),
                   typerolename=('dbcommand', 'setting', 'status', 'stats', 'aggregator', 'data'),
                   typenames=('optstype', 'type')),
        TypedField('parameters', label=l_('Parameters'),
                   names=('param', 'paramter', 'parameters'),
                   typerolename=('dbcommand', 'setting', 'status', 'stats', 'aggregator', 'data'),
                   typenames=('paramtype', 'type')),
        TypedField('fields', label=l_('Fields'),
                   names=('fields', 'fields', 'field', 'field'),
                   typerolename=('dbcommand', 'setting', 'status', 'stats', 'aggregator', 'data'),
                   typenames=('fieldtype', 'type')),
        TypedField('flags', label=l_('Flags'),
                   names=('flags', 'flags', 'flag', 'flag'),
                   typerolename=('dbcommand', 'setting', 'status', 'stats', 'aggregator', 'data'),
                   typenames=('flagtype', 'type')),
        GroupedField('errors', label=l_('Throws'), rolename='err',
                     names=('throws', ),
                     can_collapse=True),
        GroupedField('exception', label=l_('Exception'), rolename='err',
                     names=('exception', ),
                     can_collapse=True),
        Field('returnvalue', label=l_('Returns'), has_arg=False,
              names=('returns', 'return')),
        Field('returntype', label=l_('Return type'), has_arg=False,
              names=('rtype',)),
    ]

class MongoDBMethod(MongoDBObject):
    has_arguments = True

class MongoDBXRefRole(XRefRole):
    def process_link(self, env, refnode, has_explicit_title, title, target):
        # basically what sphinx.domains.python.PyXRefRole does
        refnode['mongodb:object'] = env.temp_data.get('mongodb:object')
        if not has_explicit_title:
            title = title.lstrip('.')
            target = target.lstrip('~')
            if title[0:1] == '~':
                title = title[1:]
                dot = title.rfind('.')
                if dot != -1:
                    title = title[dot+1:]
        if target[0:1] == '.':
            target = target[1:]
            refnode['refspecific'] = True
        return title, target

class MongoDBDomain(Domain):
    """MongoDB Documentation domain."""
    name = 'mongodb'
    label = 'MongoDB'
    # if you add a new object type make sure to edit MongoDBObject.get_index_string
    object_types = {
        'dbcommand':    ObjType(l_('dbcommand'),   'dbcommand'),
        'operator':     ObjType(l_('operator'),    'operator'),
        'projection':   ObjType(l_('projection'),  'projection'),
        'binary':       ObjType(l_('binary'),      'program'),
        'setting':      ObjType(l_('setting'),     'setting'),
        'status':       ObjType(l_('status'),      'status'),
        'stats':        ObjType(l_('stats'),       'stats'),
        'readmode':     ObjType(l_('readmode'),    'readmode'),
        'method':       ObjType(l_('method'),      'method'),
        'data':         ObjType(l_('data'),        'data'),
        'collflag':     ObjType(l_('collflag'),    'collflag'),
        'error':        ObjType(l_('error'),       'error'),
        'macro':        ObjType(l_('macro'),       'macro'),
        'limit':        ObjType(l_('limit'),       'limit'),
        'bsontype':     ObjType(l_('bsontype'),    'bsontype'),
    }

    directives = {
        'dbcommand':     MongoDBObject,
        'operator':      MongoDBObject,
        'projection':    MongoDBObject,
        'binary':        MongoDBObject,
        'setting':       MongoDBObject,
        'status':        MongoDBObject,
        'stats':         MongoDBObject,
        'readmode':      MongoDBObject,
        'method':        MongoDBMethod,
        'data':          MongoDBObject,
        'collflag':      MongoDBObject,
        'error':         MongoDBObject,
        'macro':         MongoDBObject,
        'limit':         MongoDBObject,
        'bsontype':      MongoDBObject,
    }
    roles = {
        'dbcommand':   MongoDBXRefRole(),
        'operator':    MongoDBXRefRole(),
        'projection':  MongoDBXRefRole(),
        'program':      MongoDBXRefRole(),
        'setting':     MongoDBXRefRole(),
        'status':      MongoDBXRefRole(),
        'stats':       MongoDBXRefRole(),
        'readmode':    MongoDBXRefRole(),
        'method':      MongoDBXRefRole(),
        'data':        MongoDBXRefRole(),
        'collflag':    MongoDBXRefRole(),
        'error':       MongoDBXRefRole(),
        'macro':       MongoDBXRefRole(),
        'limit':       MongoDBXRefRole(),
        'bsontype':    MongoDBXRefRole(),
    }
    initial_data = {
        'objects': {}, # fullname -> docname, objtype
    }

    def find_obj(self, env, obj, name, typ, searchorder=0):
        if name[-2:] == '()':
            name = name[:-2]
        objects = self.data['objects']
        newname = None

        if typ == 'program':
            name = 'bin.' + name
            newname = name

        searchorder = 1

        if searchorder == 1:
            if obj and obj + '.' + name in objects:
                newname = obj + '.' + name
            else:
                newname = name
        else:
            if name in objects:
                newname = name
            elif obj and obj + '.' + name in objects:
                newname = obj + '.' + name

        return newname, objects.get(newname)

    def resolve_xref(self, env, fromdocname, builder, typ, target, node,
                     contnode):
        objectname = node.get('mongodb:object')
        searchorder = node.hasattr('refspecific') and 1 or 0
        name, obj = self.find_obj(env, objectname, target, typ, searchorder)

        if not obj:
            return None

        if name.startswith('bin.'):
            name = name.split('.', 1)[1]

        return make_refnode(builder, fromdocname, obj[0],
                            name.replace('$', '_S_'), contnode, name)

    def get_objects(self):
        for refname, (docname, type) in self.data['objects'].items():
            yield refname, refname, type, docname, refname.replace('$', '_S_'), 1

def setup(app):
    app.add_domain(MongoDBDomain)