add 'std/printf' format (pad, reprint) utils + use them over the project | fix all api calls | add format() for each API | add global Mixer.format() to properly mix results from apis

This commit is contained in:
Adrien Marquès 2018-10-24 17:27:24 +02:00
parent f0694a6b55
commit d30c445868
8 changed files with 299 additions and 53 deletions

View File

@ -1,10 +1,14 @@
export default interface Client {
feed_query(query: object|string) : boolean;
/** Processes an API call to the given resource and returns a Promise resolving with the output object
* or rejecting the Error
*
* @param query represents the input data composing the request
*/
call(query: object|string) : Promise<object>;
get_all() : Promise<object>[];
get(i: number) : Promise<object>;
}

View File

@ -1,7 +1,8 @@
import Client from "./interface";
import Key from "./keys";
import { request } from 'https';
import printf from "../fmt/printf";
import { printf, pad, reprintf } from "../std/printf";
import { extract } from "../std/extract";
export default class CarrefourItems implements Client {
@ -9,27 +10,67 @@ export default class CarrefourItems implements Client {
static uri: string = '/v1/openapi/items';
static get key(){ return Key.get('carrefour'); }
query:string;
constructor(){
printf('+ carrefour items \t-> ');
printf('+ carrefour items');
// check key
if( !CarrefourItems.key['app_id'] || !CarrefourItems.key['app_secret'] ){
printf('error\n');
pad('error\n');
throw new Error("The 'carrefour' key must feature 2 fields: ['app_id', 'app_secret']");
}
printf('loaded\n');
pad('loaded\n');
}
format(raw:object) : object {
call(query: object|string) : Promise<object>{
if( !raw['list'] )
return {};
let fmt: object = {};
fmt['found'] = raw['list'].length
fmt['list'] = [];
for( let i = 0 ; i < raw['list'].length ; i++ ){
fmt['list'][i] = {
name: extract(`list.${i}.name`, raw),
gtin: extract(`list.${i}.gtin`, raw),
description: extract(`list.${i}.description`, raw),
quantity: {
value: extract(`list.${i}.capacity_volume`, raw) * extract(`list.${i}.capacity_factor`, raw),
unit: extract(`list.${i}.capacity_unit`, raw)
}
}
}
// /*DEBUG*/return raw;
return fmt
}
feed_query(query) : boolean {
if( typeof query != 'object' )
return false
this.query = extract('q', query)
if( this.query == null )
return false
return true
}
get(i) : Promise<object>{ return null; }
get_all() : Promise<object>[]{
// check query
if( typeof query != 'string' )
return new Promise<object>((_,reject)=>{ reject('invalid query; expected string') })
let reqdata = JSON.stringify({
queries: [ { query: query, field: 'barcodeDescription' } ]
queries: [ { query: this.query, field: 'barcodeDescription' } ]
})
let reqopt = {
@ -38,7 +79,7 @@ export default class CarrefourItems implements Client {
port: 443,
method: 'POST',
headers: {
'Accept': 'application/json',
'accept': 'application/json',
'content-type': 'application/json',
'content-length': Buffer.byteLength(reqdata),
'x-ibm-client-id': CarrefourItems.key['app_id'],
@ -46,22 +87,28 @@ export default class CarrefourItems implements Client {
}
};
return new Promise<object>( (resolve, reject) => {
return [new Promise<object>( (resolve, reject) => {
printf(' + carrefour item search | ');
printf(' + carrefour item search'); pad('0%');
let remaining = 100;
let req = request(reqopt, (res) => {
let chunks = [];
res.on('data', (chunk) => chunks.push(chunk));
res.on('data', (chunk) => {
chunks.push(chunk)
remaining = remaining / 2
reprintf(' + carrefour item search'); pad('%d%%', Math.round(100-remaining) )
});
res.on('end', () => {
let raw = Buffer.concat(chunks)
try{
let json = JSON.parse(raw.toString());
resolve(json)
reprintf(' + carrefour item search'); pad('100%\n')
resolve( this.format(json) )
}catch(e){
reprintf(' + carrefour item search'); pad('fail\n')
reject(`invalid json response: ${e.message}`);
}
@ -74,7 +121,7 @@ export default class CarrefourItems implements Client {
req.end();
});
}) ]
}

129
src/api/off.ts Normal file
View File

@ -0,0 +1,129 @@
import Client from "./interface";
import Key from "./keys";
import { request } from 'https';
import { printf, pad, reprintf } from "../std/printf";
import { extract } from "../std/extract";
import { join } from "path";
export default class OpenFoodFacts implements Client {
static host: string = 'world.openfoodfacts.org';
static uri: string = '/api/v0/product/';
list: object[];
constructor(){
printf('+ open food facts');
pad('loaded\n');
}
feed_query(query) : boolean {
// check query
if( typeof query != 'object' )
return false
this.list = query[0]['list'] || null
if( !(this.list instanceof Array) )
return false
return true
}
format(raw: object) : object {
let fmt: object = {};
if( extract('status', raw) != 1 ) return null;
fmt['product_name'] = extract('product.product_name', raw)
fmt['image'] = {
front: extract('product.image_front_small_url', raw),
nutrition: extract('product.image_nutrition_url', raw),
ingredients: extract('product.image_ingredients_small_url', raw),
}
// /*DEBUG*/return raw;
return fmt;
}
get(i) : Promise<object>{
// check key
let gtin:string = extract('gtin', this.list[i]);
if( gtin == null || gtin.length < 1 ){
return null;
}
let reqopt = {
hostname: OpenFoodFacts.host,
path: join(OpenFoodFacts.uri, `${gtin}.json`),
port: 443,
method: 'GET',
headers: {
'accept': 'application/json',
'content-type': 'application/json'
}
};
return new Promise<object>( (resolve, reject) => {
let req = request(reqopt, (res) => {
let chunks = [];
res.on('data', (chunk) => chunks.push(chunk));
res.on('end', () => {
let raw = Buffer.concat(chunks)
try{
let json = JSON.parse(raw.toString());
resolve( this.format(json) )
}catch(e){
reject(`invalid json response: ${e.message}`);
}
});
})
req.on('error', (err) => reject(err.message));
req.end();
})
}
get_all() : Promise<object>[]{
printf(' + open food facts search');
/* FOR EACH RESULT */
let promises: Promise<object>[] = [];
let success = 0;
for( let i = 0 ; i < this.list.length ; i++ ){
let promise = this.get(i)
if( promise == null ) continue;
promises.push(promise);
success++
reprintf(' + open food facts search');
pad('%d%%', Math.round(100*success/this.list.length) );
}
printf('\n')
return promises
}
}

View File

@ -1,7 +0,0 @@
import { format } from "util";
export default function printf(fmt:string, ...options:any){
process.stdout.write( format(fmt, ...options) );
}

View File

@ -1,8 +1,7 @@
import { createServer } from 'http'
import { Socket } from 'net';
import Mixer from './mixer'
import CarrefourItems from './api/items';
import printf from './fmt/printf';
import { printf } from './std/printf';
const mixer: Mixer = new Mixer();

View File

@ -3,44 +3,50 @@ import CarrefourItems from "./api/items";
import { parse as parseURL, UrlWithParsedQuery } from "url";
import { ParsedUrlQuery } from "querystring";
import Client from "./api/interface";
import printf from "./fmt/printf";
import { printf, pad } from "./std/printf";
import OpenFoodFacts from "./api/off";
export default class Mixer {
static valid_urls = ["/"];
chain: Client[] = [new CarrefourItems()];
chain: Client[] = [new CarrefourItems(), new OpenFoodFacts()];
constructor(){
printf('+ mixer \t\t-> loaded\n\n');
printf('+ mixer')
pad('loaded\n');
}
http_handler(req: IncomingMessage, res: ServerResponse){
static http_error(res: ServerResponse, reason: string){
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: -1,
reason: reason
}));
}
async http_handler(req: IncomingMessage, res: ServerResponse){
let urlObj : UrlWithParsedQuery = parseURL(req.url,true)
printf('\n -- new client --\n')
// reject invalid requests
if( req.method != 'POST' ){
if( req.method != 'POST' ) return Mixer.http_error(res, 'only POST method allowed');
else if( Mixer.valid_urls.indexOf(urlObj.pathname) < 0 ) return Mixer.http_error(res, `only URI [${Mixer.valid_urls}] allowed`);
try{
let query_result = await this.query(req, res)
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({reason: 'invalid HTTP method; only POST allowed'}))
return;
}else if( Mixer.valid_urls.indexOf(urlObj.pathname) < 0 ){
res.end(JSON.stringify( this.format(query_result) ));
}catch(err){
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({reason: `invalid URI; only [${Mixer.valid_urls}] allowed`}))
return;
res.end(JSON.stringify({reason: err.message}))
}
this.query(req, res).then((obj) => {
res.end(JSON.stringify(obj));
}).catch( err => {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({reason: `query error: ${err.toString()}`}))
})
}
@ -51,15 +57,18 @@ export default class Mixer {
for( let i = 0 ; i < this.chain.length ; i++ ){
let input = (i<1) ? query['q'] : out[i-1];
let input = (i<1) ? query : out[i-1];
try {
out[i] = await this.chain[i].call(input)
printf('ok\n')
try{
if( !this.chain[i].feed_query(input) )
throw new Error(`invalid query for api ${i}`)
out[i] = await Promise.all( this.chain[i].get_all() );
}catch(e){
printf('fail\n');
throw e
printf(' ! failure \t\t\t(%s)\n', e.message);
throw e;
}
}
@ -68,4 +77,22 @@ export default class Mixer {
}
format(raw:any) : object {
let nresults:number = raw[0][0]['found']
let results:object[] = raw[0][0]['list'];
let nimages:number = raw[1].length
// attach images
for( let i = 0 ; i < nimages ; i++ )
if( raw[1][i] != null )
results[i]['images'] = raw[1][i]['image']
return {
found: nresults,
results: results
}
}
}

18
src/std/extract.ts Normal file
View File

@ -0,0 +1,18 @@
// Key must be '.' dot-separated
export function extract(key:string, obj: object) : any{
let key_chain: string[] = key.split('.');
let pointer = obj;
for( let i = 0 ; i < key_chain.length ; i++ ){
if( pointer[key_chain[i]] == undefined )
return null
pointer = pointer[key_chain[i]]
}
return pointer
}

29
src/std/printf.ts Normal file
View File

@ -0,0 +1,29 @@
import { format } from "util";
let last_written:string = ''
const min_pad = 30;
export function printf(fmt:string, ...options:any){
last_written = format(fmt, ...options);
process.stdout.write( last_written );
}
export function reprintf(fmt:string, ...options:any){
printf('\r')
last_written = format(fmt, ...options);
process.stdout.write( last_written );
}
export function pad(fmt?:string, ...options:any){
for( let i = last_written.length ; i < min_pad ; i++ )
printf('.');
if( typeof fmt != 'string' )
return;
printf(fmt, ...options)
}