Custom adapters

In order to make the most use of the DocloopCore you can write your own custom adapters for any platform, online service or tool you like.

Every custom adapter has to provide two things:

  • an adapter class
  • an endpoint class

The adapter class is expected to extend DocloopAdapter and the endpoint class should extend DocloopEndpoint.

The method of DocloopAdapter and DocloopEndpoint marked as abstract are meant to be overwritten by your custom classes.

Example source adapter

This source adapter will provide three fixed sources and for every source that is linked to a target it will trigger a mock annotation.

var docloop         = require(docloop),
    DocloopAdapter  = docloop.DocloopAdapter,
    DocloopEndpoint = docloop.DocloopEndpoint




class ExampleSourceAdapter extends DocloopAdapter {

  //Every adapter expects the core and a configuration object as arguments for its constructor:
  constructor(core, config){
  super(core, {
    ...config,
    id:                     'example-source-adapter',
    name:                   'Example Source Adapter',
    type:                   'source',
    endpointClass:          ExampleSource, //see below
  })

  //make all stored events announce annotation every 5 seconds; see below
  setInterval( () => this.mockScan(), 5000)
  }

  //We are just returning a fixed set of endpoints. 
  //A real-life adapter should query some external resource making use of data stored with the session.
  async getEndpoints(session){
  return  [
      //creates an instance of ExampleSource with this adapter instance as adapter:
      this.newEndpoint({
        identifier: {
          adapter:  'example-source-adapter',
          name:   'source-one'
        },

        decor:{
          title:    'Source One',
          details:  'the first example source'
        }
      }),

      this.newEndpoint({
        identifier: {
          adapter:  'example-source-adapter',
          name:   'source-two'
        },

        decor:{
          title:    'Source Two',
          details:  'the second example source'
        }
      }),

      this.newEndpoint({
        identifier: {
          adapter:  'example-source-adapter',
          name:   'source-three'
        },

        decor:{
          title:    'Source Three',
          details:  'the thrid example source'
        }
      }),

    ]
  }

  //This method should actually only return stored endpoint relevant to the session, but as before, we ignore that for the example.
  async getStoredEndpoints(session){
    var raw_endpoints = await this.endpoints.find({}).toArray() 

    return raw_endpoints.map( endpoint_data => this.newEndpoint(endpoint_data))
  }


  async mockScan(){
    //make each stored source emit an annotation event on the adapter

    var sources = await this.getStoredEndpoints()

    sources.forEach( source => {
      this.emit('annotation', {
        annotation:{
          id:                123,
          sourceName:        this.name,
          sourceHome:        undefined,
          title:             "Example Annotation – "+source.identifier.name,
          author:            "Example Author",
          body:              new Date().toString(),
          respectiveContent: undefined,
          original:          source.identifier.name,
        },

        //this part makes the core reemit this event, replacing source EndpointSkeleton with a linked target EndpointSkeleton:
        source: source.skeleton
      })
    })  
  }

}


class ExampleSource extends DocloopEndpoint{

  //Every Endpoint expects an adapter instance and EndpointData as arguments for its constructor:
  constructor(adapter, data){
    super(adapter, data)
  }

  //validate the endpoint no matter what, not a good idea for a rea-life adapter!
  async validate(session_data){
    return true
  }
}

Example target adapter

This target adapter provides two fixed targets. It will listen for annotations and log corresponding data to the console.

class ExampleTargetAdapter extends DocloopAdapter {

  //Every adapter expects the core and a configuration object as arguments for its constructor:
  constructor(core, config){
    super(core, {
      ...config,
      id:                 'example-target-adapter',
      name:               'Example Target Adapter',
      type:           'target',
      endpointClass:      ExampleTarget, //see below
      endpointDefaultConfig:  {
                    doTheTargetThing: true
                  }
    })

    //Listen for annotations:
    this.core.on('annotation', this.handleAnnotation.bind(this))
  }

  async getEndpoints(session_data){
  //We are just returning a fixed set of endpoints. 
  //A real-life adapter should query some external resource making use of data stored with the session.

  return  [
        //creates an instance of ExampleTarget with this adapter instance as adapter:
        this.newEndpoint({
          identifier: {
            adapter:  'example-target-adapter',
            name:   'target-a'
          },

          decor:{
            title:    'Target A',
            details:  'the first example target'
          }
        }),

        this.newEndpoint({
          identifier: {
            adapter:  'example-target-adapter',
            name:   'target-b'
          },

          decor:{
            title:    'Target B',
            details:  'the second example target'
          }
        })
      ]
  }


  //This method should actually only return stored endpoint relevant to the session. but as before, we ignore that for the exmaple.
  async getStoredEndpoints(session){
    var raw_endpoints = await this.endpoints.find({}).toArray() 

    return raw_endpoints.map( endpoint_data => this.newEndpoint(endpoint_data))
  }

  async handleAnnotation(annotation_data){
    //check if the annotation was meant to be proccessed by this adapter; if not leave.
    if(annotation_data.target.adapter != this.id) return null

    var target  =   await this.getStoredEndpoint(annotation_data.target.id),
      a   = annotation_data.annotation

    if(target){
      console.log(`New issue for ${target.identifier.name}@${target.identifier.adapter} (spawned by ${a.original})`)
      console.log(` 
        => ${a.author} says: "Greetings from ${a.title}" 
        at ${a.body}
      `)
    }

  }

}


class ExampleTarget extends DocloopEndpoint{

  //Every Endpoint expects an adapter instance and EndpointData as arguments for its constructor:
  constructor(adapter, data){
    super(adapter, data)
  }

  //validate the endpoint no matter what, not a good idea for a rea-life adapter!
  async validate(session_Data){
    return true
  }
}

Put it all together

The core can use the two adapters like this:

//log whenever a new link is established
docloopCore.on('link-established', link => console.log(`Link established: ${link.source.adapter} <-> ${link.target.adapter}`))

docloopCore
.use(ExampleSourceAdapter)
.use(ExampleTargetAdapter)
.run()

Before all this works you have to configure the DocloopCore itself; here's an example of how this will work: README

If everything is up an running, you should be able to use the client (see README). Go ahead and select the source adapter and one of the three sources. Then select the target adapter and on eof the two targets.

Creating the link should spwan a message on the console.

From now on every five seconds the console will log the targets' processing of the annotations created by the mock scan above.