Skip to main content

Working with API data

Related links:

Adding a feed

There are two steps to adding a feed. First, a source of data must be set up at a new URL to drive the feed. This is handled by a server team, so as a content developer you will probably not have much to do here.

Second, you need to declare the feed in your deck's project.yaml file and point it at the URL set up in the first step. If you need to start work on content before the first step is completed, that's OK. Make up a URL and use the "test data" CDK feature to import data until the real data is ready.

Here's an example of a feed named my-feed in project.yaml:

feeds:
my-feed:
source: "https://feeds.com/my-feed/"

Feeds live under a top-level key called feeds. Each key is the name of a feed, and at a minimum each needs a source defining its URL. The contents at this URL will be fetched and made available to your slides under the chosen name. In this example, the feed data could be read in-slide by running Bridge.Feed.get('my-feed');.

Using variables in the feed URL

Often you'll want to have a feed give you data specific to the customer being presented to, or relating to some information collected by your fieldsets (see the customising preso creation guide). As long as the source of the feed supports such queries, this is easy! The URL declared by the source key is templated using the Django template language.

For example:

feeds:
stock-price:
source: "https://stock-data.com/?search={{ customer.title }}"

Here the hypothetical search parameter is filled out with the value of customer.title. In a preso to Frobozz Widgets, this results in the URL https://stock-data.com/?search=Frobozz Widgets. The variables available for use in templating are read from the context so anything which you put there can be used.

Here's a more complicated example. First, the context:

{
"categories": ["industrial", "commercial"],
"customer": {
"profile": {
"account_number": 14
}
}
}

And then the feed declaration:

feeds:
product-listing:
source: "https://my-company.com/api/product-list/?account={{ customer.profile.account_number|default:'public' }}&cats={{ categories|join:',' }}"

This will result in a request to the URL https://my-company.com/api/product-list/?account=14&cats=industrial,commercial. Note the use of the default and join filters. You could also have performed the same defaulting and joining logic in Javascript when populating the context; choose whichever method is easier for you. A list of available filters is available here.

Read more about templated sources in the project file overview.

Preprocessors

A preprocessor is some code which runs on the LivePreso server and modifies your feed just before the data is fetched. The most common reason to do this is so the server can add authentication to your request. You can ask for preprocessors to be run like so:

feeds:
my-feed:
source: "/api/roi/?account_id={{ account_id }}"
preprocessors:
- UseServerHostname
- AuthenticateWithLivePresoApi

See the Project file feeds reference for which preprocessors are available.

Disabling feeds

Sometimes it won't make sense to fetch a feed for a particular customer or circumstance. If you have a feed which only needs to be fetched sometimes, set the enabled_lookup attribute of the feed in your project file.

The lookup should be a dotted path into the context. Resolving that path uses the same rules as template parameters in the source of your feed, though it does not require any surrounding braces:

# project.yaml
feeds:
my-very-good-feed:
source: "...",
enabled_lookup: "foo.bar.veryGoodFeeds"

This lookup would keep the feed enabled when used with a context like this:

{
"foo": {
"bar": { "veryGoodFeeds": true }
}
}

If the context value referenced by the lookup is truthy, the feed will be fetched. If the value is absent or falsey, the feed will not be fetched (though if you do not provide an enabled_lookup at all, the feed will always be fetched).

Note the truthiness of the referenced value is determined by Python. For most JSON-serialisable values both languages agree, but note they differ for empty arrays and empty objects:

JSON valueJavaScriptPython
nullfalsefalse
falsefalsefalse
truetruetrue
0falsefalse
1truetrue
"" (Empty string)falsefalse
" " (Nonempty string)truetrue
[] (Empty array)truefalse
{} (Empty object)truefalse
[] (Nonempty array)truetrue
{} (Nonempty object)truetrue

Accessing feed data from your deck

Now that your project file is set up and you're finally writing a slide, here's how to get your data. First, use Bridge.Feed.get to get your hands on a feed object.

var feed = Bridge.Feed.get("my-feed");

Once you have a feed object, you can start pulling data from it. Feed objects use a CSS-like language called JSONSelect to allow this. You should pass a JSONSelect "selector" to Feed.match or Feed.match_many functions to get data from a feed object.

// this is the raw JSON content of the "my-feed" feed used in the following
// example
{
"users": [
{"user": {"name": "Art Vandelay", "hat_size": 14}},
{"user": {"name": "Pennypacker"}}
],
"cats": [
{"name": "badger"},
{"name": "timecat"}
]
}

var feed = Bridge.Feed.get('my-feed');

// select all names
var names = feed.match_many('.name', ['no names']);
// ["Art Vandelay", "Pennypacker", "badger", "timecat"]

// select the names of users, ignoring the cats.
var usernames = feed.match_many('.user .name', ['no names']);
// ["Art Vandelay", "Pennypacker"]

In the example note that a default value ["no names"] is used. You are always required to provide default values as part of robust selection.

Robust selection

It's important that your deck interact with feeds in a robust way. You should be able to handle the following situations, without breaking.

  1. A feed disappears entirely
  2. A feed contains less or more fields than expected
  3. A matched selection contains more than one item, where only one was expected

A good deal of this robustness comes from named traversal of JSON data as provided by JSONSelect. This prevents less or more fields, or a changed field order, from breaking the selection of the correct field. Nice one, you don't have to do anything!

In fact, there are only two things you need to do:

  • First, you must decide whether you expect a single value or multiple values every time you select something from the feed, then use Feed.match or Feed.match_many appropriately.
  • Second, you must provide a default value whenever you use either of these functions, in case there are unexpected problems with the feed.

Sidestepping defaults

If you'd like to handle default values in a special way, you can manually disable the protections Matcher provides by setting use_defaults to false:

var prices = Bridge.Feed.get("product-prices");
prices.use_defaults = false;
var telephone_price = prices.match(".products .price");

Now match will return whatever it matches, whether that be nothing, one value, or many. While use_defaults is disabled, there's no difference between the match and match_many methods.

Bypassing JSONSelect

You can grab the raw feed JSON if you want to get your hands dirty:

var jsondata = Bridge.Feed.get("data-feed").raw();

Logging

When feeds are used in a slide you can view logging data about them in the console.

The messages will usually take this form:

feeds | [debug] Slide Title
feeds | [debug] .key_name returned data: 100
feeds | [warn] .another_key_name returned (fallback) data: 0

The first line will be the title of the slide and will always be a debug message, visible only in the CDK and non-production versions of the app.

Each subsequent line will display the key used to match feed data and the result(s) returned.

If the data was found and returned as expected, a debug message will be logged and will show the data found. If the key did not have a feed match, a warning message will be shown and the fallback data will be displayed.

Adding fixtures

Fixtures are static data files that provide a source of data to the deck when live data is not available. Fixtures for either feeds and/or context can be supplied, taking the place of data that would be gathered during the preso creation process (in the case of feeds) or generated during fieldsets & while the preso is in use (in the case of context values).

Use cases

The primary uses of fixtures are:

If fixtures are present, they are loaded by the screenshotter, automatically populating the relevant Bridge API endpoints and making them accessible to the deck. This removes the need for deck code to test whether it is running in 'screenshot-thumbnail' mode and provide dummy data to allow slides to render in this mode.

Fixtures data can also be utilised in the CDK via the context and Feeds data-source dropdowns.

image

Location on disk & configuration

The location on disk within the project folder is your discretion; by convention a fixtures directory is a logical location.

.
└── fixtures
├── feeds.json
└── context.json

The relative path is then configured as a top-level key within the project.yaml:

fixtures:
feeds: fixtures/feeds.json
context: fixtures/context.json

Both entries are optional.

Structure of files

Feeds

The feeds fixture file on disk is a JSON document with top level keys corresponding to the ones defined in the feeds section of the deck's project.yaml.

Context

The context fixture file on disk is a JSON document with a structure that mimics the context you wish to create. In this way, it has the same structure as context Test Data as defined in the CDK.

Generating fixture files

The relevant JSON structures can be handcrafted to generate a fixture on disk.

As an alternative to this, a useful mechanism to generate fixture files is at runtime, either within the CDK or the app, by taking a snapshot of the values currently held in memory. The following code, pasted into the console of either, will generate a function that can capture fixture data to disk:

var extractFixturesTo = function (feedKey, fileName) {
var feedBlob = Bridge.Feed.feeds[feedKey];
var contextBlob = Bridge.Context.getDict();
var fs = nw.require("fs");
var path = nw.require("path");
var os = nw.require("os");
var outputFileFeeds = path.resolve(os.homedir(), fileName + "-feeds.json");
fs.writeFileSync(outputFileFeeds, JSON.stringify(feedBlob, null, 2));
var outputFileContext = path.resolve(
os.homedir(),
fileName + "-context.json"
);
fs.writeFileSync(outputFileContext, JSON.stringify(contextBlob, null, 2));
};

You can then call the following from the console to capture fixtures to disk:

extractFixturesTo("mydeckkey", "sample");

Where mydeckkey is the deck key defined in your project.yaml. This will generate sample-feeds.json and sample-context.json in the current user's home directory. These can then be copied to the project directory and referenced in the project.yaml.

Bonus use case

When fixtures are utilised in the CDK via the context and Feeds data-source dropdowns, the paths in the project.yaml can be edited. This can be a useful means swap out a complete dataset when a number of feeds are defined, by keeping multiple fixture files on disk and switching between them by editing the project.yaml to change the paths referenced there. Some care must be taken to remove unused fixture files on disk before deck upload, to avoid package sizes from bloating.