It is currently April 5th, 2020, 2:46 am

Parsing JSON API

Tips and Tricks from the Rainmeter Community
stefansh
Posts: 3
Joined: January 22nd, 2020, 8:19 pm

Parsing JSON API

Post by stefansh »

I set out to write my first Rainmeter skin and found that people were using Perl Regular Expressions to parse JSON. This is problematic for a number of reasons:
  • JSON format doesn't guarantee ordering or spacing so an API is free to keep the same semantics but rearrange fields. This will randomly break regular expression-based skins.
  • Modification of and debugging non-trivial Regular Expressions is cumbersome, though there are some pretty cool tools for that.
  • The inherent complexity of Regular Expressions puts this type of development out of reach for a lot of people.
  • There are plenty of JSON parsing libraries that are well-tested and maintained and are performant.
Note: XML has most of the same problems and can be parsed using a similar method as mine

I searched for a solution and didn't find one so I came up with my own. Here's a fully-functional skin using this technique. The essence follows.

Given this excerpt of the Darksky weather API JSON response:

Code: Select all

{
    "timezone": "America/New_York",
    "currently": {
        "time": 1579389931,
        "summary": "Light Snow",
        "icon": "snow",
        "temperature": 31.06,
        "apparentTemperature": 25.12,
        "dewPoint": 29.5,
        "humidity": 0.94,
        "pressure": 1018.1,
        "visibility": 0.926,
        "ozone": 300
    },
    "hourly": {
        "summary": "Light rain until tomorrow morning.",
        "icon": "rain",
        "data": [
            {
                "time": 1579388400,
                "summary": "Light Snow",
                "icon": "snow",
                "precipIntensity": 0.0516,
            }
    }
}
Before (the old way)

weather.ini

Code: Select all

[Weather]
Measure=WebParser
URL=https://api.darksky.net/forecast/#APIKey#/#Location#
RegExp=(?siU).*"currently":\{.*"summary":"([^"]*)"[^}]*"icon":"([^"]*)"[^}]*"temperature":([^\.]*)(\.|,).*}[^}]*}.*,"hourly":{"summary":"([^"]*)"

[Summary]
Measure=WebParser
URL=[Weather]
StringIndex=1

[Icon]
Measure=WebParser
URL=[Weather]
StringIndex=2

[Temperature]
Measure=WebParser
URL=[Weather]
StringIndex=3

[Hourly]
Measure=WebParser
URL=[Weather]
StringIndex=5

[SummaryMeter]
Meter=String
MeasureName=Summary

[IconMeter]
Meter=Image
MeasureName=Icon
ImageName=%1.png

[TemperatureMeter]
Meter=String
MeasureName=Temperature

[HourlyMeter]
Meter=String
MeasureName=Hourly
After (the new way)

weather.ini

Code: Select all

[Weather]
Measure=WebParser
URL=https://api.darksky.net/forecast/#APIKey#/#Location#
RegExp=(?sU)^(.*)$
StringIndex=0
FinishAction=!UpdateMeasure ProcessData

[ProcessData]
Measure=Script
ScriptFile=#@#Scripts\weather.lua
UpdateDivider=-1

; Don't need measure sections :)

[Summary]
Meter=String

[Icon]
Meter=Image

[Temperature]
Meter=String

[Hourly]
Meter=String
weather.lua

Code: Select all

function Initialize()
	dofile( SKIN:GetVariable( '@' )..'Scripts\\json.lua' )
	weatherData = SKIN:GetMeasure( "Weather" )
end

function Update()
	data = decodeData( weatherData:GetStringValue() )

	SKIN:Bang( '!SetOption', 'Temperature', 'Text', data[ 'currently' ][ 'temperature' ] )
	SKIN:Bang( '!SetOption', 'Icon', 'ImageName', "#@#Images\\" .. data[ 'currently' ][ 'icon' ] )
	SKIN:Bang( '!SetOption', 'Summary', 'Text', data[ 'currently' ][ 'summary' ] )
	SKIN:Bang( '!SetOption', 'Hourly', 'Text', data[ 'hourly' ][ 'summary' ] )
end
json.lua

Get from https://github.com/rxi/json.lua

---

Questions and feedback are welcome.