Showing posts with label #smalltalk. Show all posts
Showing posts with label #smalltalk. Show all posts

Monday, 21 October 2019

Smalltalking to RESTful Service (NeoJSON)


I am using the Weather API from US National Weather Service (https://www.weather.gov/documentation/services-web-api), it's a very simple API that even doesn't require authentication.  To get the weather of a location, it's a HTTP GET based on a long and lat as https://api.weather.gov/points/39.7456,-97.0892

Calling the https://api.weather.gov/points/39.7456,-97.0892 return a JSON with a "propeties" element, inside the "properties", the "forecast" is the URL (i.e. https://api.weather.gov/gridpoints/TOP/31,80/forecast) to get the actual forecast by period.


1. Getting Response as String


The below example making use of ZnClient to make a HTTP Restful GET and return the response as a ByteString

| response url |
url := 'https://api.weather.gov/gridpoints/TOP/31,80/forecast'.
response := ZnClient new
url: url;
    enforceHttpSuccess: true;
    accept: ZnMimeType applicationJson;
get.

^ response

2. Getting Response as Structured Objects


First of all, we want to serialize and deserialize between JSON and Smalltalk objects.  I found a very good Open Source framework - NeoJSON (https://github.com/svenvc/NeoJSON).

NeoJSON can be loaded by the "Doing" the following ...

Gofer it
   smalltalkhubUser: 'SvenVanCaekenberghe' project: 'Neo';
   configurationOf: 'NeoJSON';
   loadStable.

NeoJSON has a Reader (i.e. NeoJSONReader) that by default,  map JSON response to String, Array and Dictionary.

| response url |
url := 'https://api.weather.gov/gridpoints/TOP/31,80/forecast'.
response := ZnClient new
url: url;
    enforceHttpSuccess: true;
    accept: ZnMimeType applicationJson;
contentReader: [:entity| NeoJSONReader fromString: entity readStream];
get.

^ response

Inspect the execution of above example showing a dictionary of nest array and dictionaries


3. Mapping to a Schema

Here we created a Smalltalk class - ForecastResponse that has three instance variables; geometry, properties and type.  NeoJSONReader create and populate the Schema object using reflection.

url := 'https://api.weather.gov/gridpoints/TOP/31,80/forecast'.
response := ZnClient new
url: url;
    enforceHttpSuccess: true;
    accept: ZnMimeType applicationJson;
contentReader: [:entity|
|reader|
reader:=(NeoJSONReader on: entity readStream).
reader mapInstVarsFor: ForecastResponse.
reader nextAs: ForecastResponse.
];
get.

Inspect the execution of above example showing #type is populated with String, #geometry and #properties are nested dictionaries as we didn't provide any specific mapping for them.


4. Mapping to a Nested Schema

The #properties returned above consisting of nested complex structures of key values pair and array of detail forecast info (i.e. #periods), here we created a Smalltalk class - Forecast to represent the #properties JSON object.

The Forecast object has the following declaration ...
Object subclass: #Forecast
instanceVariableNames: 'updated units updateTime validTimes elevation periods'
classVariableNames: ''
package: 'Weather-API'

Herewith we specify the mapping for the #properties JSON...

url := 'https://api.weather.gov/gridpoints/TOP/31,80/forecast'.
response := ZnClient new
url: url;
    enforceHttpSuccess: true;
    accept: ZnMimeType applicationJson;
contentReader: [:entity|
|reader|
reader:=(NeoJSONReader on: entity readStream).
reader for: ForecastResponse do: [ :mapping |
mapping mapInstVar: #type.
mapping mapInstVar: #geometry.
(mapping mapInstVar: #properties) valueSchema: Forecast
].
reader mapInstVarsFor: Forecast.
reader nextAs: ForecastResponse.
];
get.


Inspecting the execution of above example showing the #properties is a Forecast instance, however the #periods is still an array of dictionaries.



5. Mapping to Nested Schema Array


Another Smalltalk Class - ForecastPeriod is declared to represent the forecast details nested as a dictionary in the #periods array.

Object subclass: #ForecastPeriod
instanceVariableNames: 'name icon isDaytime windSpeed startTime temperatureUnit detailedForecast shortForecast temperatureTrend temperature endTime windDirection number'
classVariableNames: ''
package: 'Weather-API'

A nested mapping whereas the whole JSON response is mapped to nested levels of

  • ForecastResponse
    • Forecast
      • ForecastPeriod
Array of dictionaries is mapped to #listOfElementSchema by the Reader's #for:customDo: message.


url := 'https://api.weather.gov/gridpoints/TOP/31,80/forecast'.
response := ZnClient new
url: url;
    enforceHttpSuccess: true;
    accept: ZnMimeType applicationJson;
contentReader: [:entity|
|reader|
reader:=(NeoJSONReader on: entity readStream).
reader for: ForecastResponse do: [ :mapping |
mapping mapInstVar: #type.
mapping mapInstVar: #geometry.
(mapping mapInstVar: #properties) valueSchema: #ForecastSchema
].
reader for: #ForecastSchema do: [ :mapping|
mapping subjectClass: Forecast.
mapping mapInstVars.
(mapping mapInstVar: #periods) valueSchema: #ArrayOfForecast.
].
reader for: #ArrayOfForecast customDo: [ :mapping| mapping listOfElementSchema: ForecastPeriod ].
reader mapInstVarsFor: ForecastPeriod.
reader nextAs: ForecastResponse.
];
get.

Inspecting the execution of above example showing the #properties is a Forecast instance with its #periods consisting of an array of ForecastPeriod instances.



Note: Source available on ...
https://github.com/kichoi/backto-smalltalk-pharo/tree/master/Weather-API.package