Help

Building your own macro

Getting started with the Macro Development Toolkit

These instructions are for building a custom macro in Mingle using the Macro Development Toolkit. The toolkit is available as a gem and is available either with your Mingle download or via the web.

If installing from the local gem you can install from the folder containing the gem as follows:

% [sudo] gem install mingle-macro-development-toolkit-1.2.gem

If you are installing this gem from its home on RubyForge then you can install it using:

% [sudo] gem install mingle-macro-development-toolkit

A background on macros in Mingle

Macros are a special kind of markup in the Mingle wiki that can be used on cards and wiki pages.

A macro is identified by the following markup generic syntax:

{{ 
  macro_name
    parameter1: value1
    parameter2: value2
    ...
}}

The markup has to be valid YAML syntax. Specifically, this means that the markup is sensitive to spacing and indentation.

For more help around YAML and what constitutes valid YAML markup you can refer to http://yaml.org/spec/current.html.

Specific examples of macros include the pre-written Mingle macros such as the value, average & table macros and the macros for all the charts.

When Mingle encounters a macro while rendering the markup it delegates handling of the macro to a custom class, behind the scenes, that is registered to handle it. For example, if Mingle encountered the following markup it would parse the content between the opening and closing double braces.

{{
  average
    query: SELECT 'Pre-release Estimate' WHERE Release = (current release)
}}

Parsing would identify the following:

macro name: average

macro parameters: {query => "SELECT 'Pre-release Estimate' WHERE Release = (current release)"}

Mingle then scans a registry of known macros for a class that is configured to handle a macro with the name "average" and would identify the AverageMacro class. You can find this class under the /vendor/plugins/average_macro directory of your installation of Mingle.

class AverageMacro
  
  def initialize(parameters, project, current_user)
    @parameters = parameters
    @project = project
    raise "Parameter query is required" unless query
  end
  
  def execute
    first_values = @project.execute_mql(query).collect { |record| record.values.first }
    data = first_values.reject(&:blank?).collect(&:to_f) 
    data.empty? ? 'no values found' : @project.format_number_with_project_precision(data.sum.to_f/data.size.to_f)
  end
  
  private
  
  def query
    @parameters['query']
  end
end

All the data that is required to execute the macro is injected into the macro through the constructor. The parameters that are interpreted from the markup are passed in as a hash. The project that is passed in is a lightweight representation of the project in the Mingle model and is documented at http://mingle-macros.rubyforge.org/rdoc.

The execute method uses the MQL execution facility, that the project class provides, to execute the MQL string that is passed into through the parameters hash. It then formats the results to be a number and provides that as the result of the execute command.

For more help on what constitutes valid MQL you can refer to our MQL reference documentation.

Writing your own macro

To write your own macro, you can start with a generated skeleton for the macro. Use the new_mingle_macro script, that installed with your gem to generate your own macro skeleton, as follows:

% new_mingle_macro your_new_macro

This generates a folder structure as follows:

your_new_macro
      |
      |----Rakefile
      |
      |----init.rb
      |
      |----lib
      |     |
      |     your_new_macro.rb
      |
      |
      |----test
             |
             |----fixtures
             |      |
             |    sample
             |       |
             |       projects.yml, card_types.yml...
             |
             |---- unit
             |      |
             |      your_new_macro_test.rb,...
             |
             |---- integration
                    |
                    your_new_macro_integration_test.rb, ...

The lib directory contains the actual macro and the test folders give you the option to run the tests either against local YAML based fixtures or using REST to test against a deployed mingle instance.

When this macro is deployed to Mingle all wiki markup in the following form will be parsed as YAML and handling will be delegated to an instance of your macro class YourNewMacro.

{{
  your_new_macro
    parameter1: value1
    parameter2: <some_mql_snippet>
    ...
}}

The parameters will be parsed into a Ruby hash of the following structure and will be passed into the constructor of the class along with an instance of a Mingle::Project, that represents the project that this macro is being rendered on.

{'parameter1' => 'value1', 'parameter2' => '<some_mql_snippet>'}

An example of what you can do with this information is the following macro which uses the Google Charting API to render a Google-o-meter style chart to represent work completed in a fuel guage style meter.

class WorkGuage
  
  def initialize(parameters, project, current_user)
    @parameters = parameters
    @project = project
    @current_user = current_user
  end
    
  def execute
    completed_work = @project.execute_mql(params['completed_work']).first.values.sum
    total_work = @project.execute_mql(params['total_work']).first.values.sum
    completion_percentage = (completed_work.to_f / total_work.to_f) * 100
    
    %Q{ <img src='http://chart.apis.google.com/chart?cht=gom&chs=350&chd=t:#{completion_percentage}&chds=0,100' /> }
  end
  
end

The execute method can return any valid HTML or Javascript content. The ability to emit Javascript content, specifically, can only be used on Mingle version 2.3 and above.

The following example demonstrates how to use the Javascript emission functionality to embed a Google Maps map.

For this example to work, you will have to supply your own Google Maps API key as a part of the script tag that loads the Google API.

class GoogleMap

  def initialize(parameters, project, current_user)
    @latitude = parameters['latitude'] || 39.55
    @longitude = parameters['longitude'] || 116.25
    @zoom_level = parameters['zoom_level'] || 8
  end

  def execute
  <<-HTML
    h2. Google Maps JavaScript API Example: Simple Map
    
    <div id="map_canvas" style="width: 600px; height: 400px"></div>
    <script src="http://maps.google.com/maps?file=api" type="text/javascript"></script>
    <script type="text/javascript">
      // register the initialize function for executing after page loaded.
      MingleJavascript.register(function initialize() {
        if (GBrowserIsCompatible()) {
          var map = new GMap2(document.getElementById("map_canvas"));
          map.setCenter(new GLatLng(#{@latitude}, #{@longitude}), #{@zoom_level});
        }
      })
    </script>
    HTML
  end

  def can_be_cached?
    false  # if appropriate, switch to true once you move your macro to production
  end
end

Long running or integration macros which run on server will result in long page render times. We recommend these sort of macros use javascript or JSONP for cross-domain.

You can find both simpler and more complex examples in the /vendor/plugins/sample_macros directory.

Unit testing your macro

The Macro Development Toolkit comes with a built-in unit testing framework that borrows the familiar idea of YAML based fixtures. The one small difference made is that each project, that you are providing fixtures for, gets its own subfolder within the fixtures directory. We hope that this makes it easier to identify relationships between the objects set up in the YAML files.

If you are using the skeleton project set up by the new_mingle_macro script, the fixtures directory provides you with a sample project fixture. The data in that should give you a sense of the relationships between the various objects.

The skeleton project also has a sample unit test set up for you which uses the sample fixture data. Note the helper method project(...) which takes the name of a sample project to load information from. This method loads a web of objects from the directory named the same as the argument, in the fixtures folder.

class YourNewMacroTest < Test::Unit::TestCase
  
  FIXTURE = 'sample'
  
  def test_macro_contents
    macro = YourNewMacro.new(nil, project(FIXTURE), nil)
    result = macro.execute
    assert result
  end

end

Once loaded, you can test things like parameter checking and validations using this style of test. While you cannot execute MQL in this style of test, you can use your favourite mocking library to test how results get handled.

You can see examples of unit tests in the average macro that is packaged with Mingle in the /vendor/plugins/average_macro directory.

To run your unit tests, run the following from the root of your custom macro:

% rake test:units 

Integration testing your macro

In order to run the integration tests, you will need to turn on basic authentication for the Mingle 2.3 server that you are going to be testing against.

The integration tests look very similar to the unit tests, the primary difference being that they actually communicate with a deployed Mingle instance over REST. The helper methods populate a web of objects representing a project that look and behave in a manner identical to how they will in production.

The one significant difference about these style of tests is that you can actually execute MQL remotely on the Mingle instance instead of mocking out the MQL execution. This will give you a good idea of what results and errors you may expect to see in production without having to deploy the macro every time. There are tradeoffs, of course, and some of these are:

  • should you decide to add these tests to a Continuous Integration build, like Cruise, you will hit the production Mingle server with every test run. Not hot.
  • given that each test makes a call to a production server there is no guarantee, unless you set it up in such a way, that multiple calls to fetch the same resource will give the same result.
  • also, while not slow, these tests are definitely much slower than the unit tests and so, while it is certainly possible to write only integration tests, we would encourage a judicious mix of both styles.
class YourNewMacroIntegrationTest < Test::Unit::TestCase
  
  PROJECT_RESOURCE = 'http://yourname:password@your.mingle.server:port/lightweight_project/project_identifier.xml'

  def test_macro_contents
    macro = YourNewMacro.new(nil, project(PROJECT_RESOURCE), nil)
    result = macro.execute
    assert result
  end

end

The skeleton project also has a sample integration test set up for you which points to a bogus Mingle server and uses bad credentials. Replace this resource URL with the URL for a deployed instance within your organization. The helper method project(...) which takes the resource URL loads the data from the XML data obtained from the live instance of Mingle.

You can see examples of integration tests in the average macro that is packaged with Mingle in the /vendor/plugins/average_macro directory. These tests run against a standard template that ships with Mingle 2.3 and so you should be able to run them within your organization without a problem.

To run your integration tests, run the following from the root of your custom macro:

% rake test:integration 

Deploying your macro

Before you deploy anything to your Mingle instance, please make sure that it is completely safe. This is especially important if the macro was developed by a third party. Here is a list of things that you should look out for. This list should not be considered complete, it is just a representative sample.

  • If Mingle runs as a privileged user, the macro could end up damaging the host machine
  • Through direct SQL calls, rather than using supplied MQL execution mechanism, the macro could gain access to data that people would normally not be authorized to see
  • Lengthy calls to external systems could tie up Mingle resources and leave the app unresponsive
  • System calls, if used, must be inspected and well-understood prior to deployment
  • Other database activity, such as transaction commits, should be monitored and avoided

To deploy your macro to a locally deployed instance of Mingle which is running at mingle_root run:

% rake macro:deploy MINGLE_LOCATION=/path/to/mingle_root

where /path/to/mingle_root is the location where the Mingle 2.3 server is installed.

    The location that the Mingle is normally installed at:
  • On Windows, this is the location that the installer installed Mingle at, typically within C:\Program Files
  • On OSX, this will be within the app bundle, at <mingle_application_bundle>/Contents/Resources/app
  • on *NIX is in the expanded archive

The entire macro folder and its contents will be copied over into the /vendor/plugins directory of that Mingle installation. Once deployed, the server will need to be restarted in order for the macro to become available for use. Alternatively, you could also copy the folder by hand into the same location.

Uninstalling the Macro Development Toolkit

To uninstall the locally installed gem run:

% [sudo] gem uninstall mingle-macro-development-toolkit

Legal notices and information

This Getting Started file and the mingle-macro-development-toolkit-1.2.gem are owned exclusively by ThoughtWorks, Inc., and ThoughtWorks reserves all rights therein.

We believe that it is a sound practice from legal, business and software development perspectives to always provide copyright information and license information with any software that you make available to others. We have provided this information for the Mingle Macro Development Toolkit in the LICENSE.txt file distributed with the Toolkit. We encourage you to use that as an example to follow when marking your software with a copyright notice, as well as providing users with a license to your software. We have chosen to use the MIT License, an Open Source License, you may choose to use the same or a different license. If you are going to use an Open Source License we strongly encourage you to use a license approved by the Open Source Initiative, available here: http://www.opensource.org/licenses.