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 value | JavaScript | Python |
---|---|---|
null | false | false |
false | false | false |
true | true | true |
0 | false | false |
1 | true | true |
"" (Empty string) | false | false |
" " (Nonempty string) | true | true |
[] (Empty array) | true | false |
{} (Empty object) | true | false |
[] (Nonempty array) | true | true |
{} (Nonempty object) | true | true |
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.
- A feed disappears entirely
- A feed contains less or more fields than expected
- 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:
- Deck upload when screenshotting thumbnails
- Editing slides in PresoManager
- Previewing slides in Empty Preview mode
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.
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.