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:
parent
f0694a6b55
commit
d30c445868
|
@ -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>;
|
||||
|
||||
}
|
|
@ -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();
|
||||
|
||||
|
||||
});
|
||||
}) ]
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
import { format } from "util";
|
||||
|
||||
export default function printf(fmt:string, ...options:any){
|
||||
|
||||
process.stdout.write( format(fmt, ...options) );
|
||||
|
||||
}
|
|
@ -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();
|
||||
|
|
79
src/mixer.ts
79
src/mixer.ts
|
@ -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')
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
Loading…
Reference in New Issue