Source: lib/client/proxy-interface.js

let EventEmitter = require('events');
const {
  isInterfaceNameValid,
  isMemberNameValid
} = require('../validators');

/**
 * A class to represent a proxy to an interface exported on the bus to be used
 * by a client. A `ProxyInterface` is gotten by interface name from the {@link
 * ProxyObject} from the {@link MessageBus}. This class is constructed
 * dynamically based on the introspection data on the bus. The advertised
 * methods of the interface are exposed as class methods that take arguments
 * and return a Promsie that resolves to types specified by the type signature
 * of the DBus method. The `ProxyInterface` is an `EventEmitter` that emits
 * events with types that are specified by the type signature of the DBus
 * signal advertised on the bus when that signal is received.
 *
 * If an interface method call returns an error, `ProxyInterface` method call
 * will throw a {@link DBusError}.
 *
 * @example
 * // this demonstrates the use of the standard
 * // `org.freedesktop.DBus.Properties` interface for an interface that exports
 * // some properties.
 * let bus = dbus.sessionBus();
 * let obj = await bus.getProxyObject('org.test.bus_name', '/org/test/path');
 * let properties = obj.getInterface('org.freedesktop.DBus.Properties');
 * // the `Get` method provided by this interface takes two strings and returns
 * // a Variant
 * let someProperty = await properties.Get('org.test.interface_name', 'SomeProperty');
 * // the `PropertiesChanged` signal provided by this interface will emit an
 * // event on the interface with its specified signal arguments.
 * properties.on('PropertiesChanged', (props, invalidated) => {});
 */
class ProxyInterface extends EventEmitter {
  /**
   * Create a new `ProxyInterface`. This constructor should not be called
   * directly. Use {@link ProxyObject#getInterface} to get a proxy interface.
   */
  constructor(name, object) {
    super();
    this.$name = name;
    this.$object = object;
    this.$properties = [];
    this.$methods = [];
    this.$signals = [];
    this.$listeners = {};

    let getEventDetails = (eventName) => {
      let signal = this.$signals.find((s) => s.name === eventName);
      if (!signal) {
        return [null, null];
      }

      let detailedEvent = JSON.stringify({
        path: this.$object.path,
        interface: this.$name,
        member: eventName
      });

      return [signal, detailedEvent];
    }

    this.on('removeListener', (eventName, listener) => {
      let [signal, detailedEvent] = getEventDetails(eventName);

      if (!signal) {
        return;
      }

      this.$object.bus._removeMatch(this._signalMatchRuleString(eventName));
      this.$object.bus._signals.removeListener(detailedEvent, this._getEventListener(signal));
    });

    this.on('newListener', (eventName, listener) => {
      let [signal, detailedEvent] = getEventDetails(eventName);

      if (!signal) {
        return;
      }

      this.$object.bus._addMatch(this._signalMatchRuleString(eventName));
      this.$object.bus._signals.on(detailedEvent, this._getEventListener(signal));
    });
  }

  _signalMatchRuleString(eventName) {
    return `type='signal',sender=${this.$object.name},interface='${this.$name}',path='${this.$object.path}',member='${eventName}'`
  }

  _getEventListener(signal) {
    if (this.$listeners[signal.name]) {
      return this.$listeners[signal.name];
    }

    let obj = this.$object;
    let bus = obj.bus;

    this.$listeners[signal.name] = (msg) => {
      let {body, signature, sender} = msg;
      if (bus._nameOwners[obj.name] !== sender) {
        return;
      }
      if (signature !== signal.signature) {
        console.error(`warning: got signature ${signature} for signal ${iface.$name}.${signal.name} (expected ${signal.signature})`);
        return;
      }
      this.emit.apply(this, [signal.name].concat(body));
    };

    return this.$listeners[signal.name];
  }

  static _fromXml(object, xml) {
    if (!xml.hasOwnProperty('$') || !isInterfaceNameValid(xml['$'].name)) {
      return null;
    }

    let name = xml['$'].name;
    let iface = new ProxyInterface(name, object)

    if (Array.isArray(xml.property)) {
      for (let p of xml.property) {
        // TODO validation
        if (p.hasOwnProperty('$')) {
          iface.$properties.push(p['$']);
        }
      }
    }

    if (Array.isArray(xml.signal)) {
      for (let s of xml.signal) {
        if (!s.hasOwnProperty('$') || !isMemberNameValid(s['$'].name)) {
          continue;
        }
        let signal = {
          name: s['$'].name,
          signature: ''
        };

        if (Array.isArray(s.arg)) {
          for (let a of s.arg) {
            if (a.hasOwnProperty('$') && a['$'].hasOwnProperty('type')) {
              // TODO signature validation
              signal.signature += a['$'].type;
            }
          }
        }

        iface.$signals.push(signal);
      }
    }

    if (Array.isArray(xml.method)) {
      for (let m of xml.method) {
        if (!m.hasOwnProperty('$') || !isMemberNameValid(m['$'].name)) {
          continue;
        }
        let method = {
          name: m['$'].name,
          inSignature: '',
          outSignature: ''
        };

        if (Array.isArray(m.arg)) {
          for (let a of m.arg) {
            if (!a.hasOwnProperty('$') || typeof a['$'].type !== 'string') {
              continue;
            }
            let arg = a['$'];
            if (arg.direction === 'in') {
              method.inSignature += arg.type;
            } else if (arg.direction === 'out') {
              method.outSignature += arg.type;
            }
          }
        }

        // TODO signature validation
        iface.$methods.push(method);

        iface[method.name] = function(...args) {
          let objArgs = [
            name,
            method.name,
            method.inSignature,
            method.outSignature
          ].concat(args);
          return object._callMethod.apply(object, objArgs);
        }
      }
    }

    return iface;
  }
}

module.exports = ProxyInterface;