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 {
|
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
|
/** Processes an API call to the given resource and returns a Promise resolving with the output object
|
||||||
* or rejecting the Error
|
* or rejecting the Error
|
||||||
*
|
*
|
||||||
* @param query represents the input data composing the request
|
* @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 Client from "./interface";
|
||||||
import Key from "./keys";
|
import Key from "./keys";
|
||||||
import { request } from 'https';
|
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 {
|
export default class CarrefourItems implements Client {
|
||||||
|
|
||||||
|
@ -9,27 +10,67 @@ export default class CarrefourItems implements Client {
|
||||||
static uri: string = '/v1/openapi/items';
|
static uri: string = '/v1/openapi/items';
|
||||||
static get key(){ return Key.get('carrefour'); }
|
static get key(){ return Key.get('carrefour'); }
|
||||||
|
|
||||||
|
query:string;
|
||||||
|
|
||||||
constructor(){
|
constructor(){
|
||||||
printf('+ carrefour items \t-> ');
|
printf('+ carrefour items');
|
||||||
|
|
||||||
// check key
|
// check key
|
||||||
if( !CarrefourItems.key['app_id'] || !CarrefourItems.key['app_secret'] ){
|
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']");
|
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({
|
let reqdata = JSON.stringify({
|
||||||
queries: [ { query: query, field: 'barcodeDescription' } ]
|
queries: [ { query: this.query, field: 'barcodeDescription' } ]
|
||||||
})
|
})
|
||||||
|
|
||||||
let reqopt = {
|
let reqopt = {
|
||||||
|
@ -38,7 +79,7 @@ export default class CarrefourItems implements Client {
|
||||||
port: 443,
|
port: 443,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json',
|
'accept': 'application/json',
|
||||||
'content-type': 'application/json',
|
'content-type': 'application/json',
|
||||||
'content-length': Buffer.byteLength(reqdata),
|
'content-length': Buffer.byteLength(reqdata),
|
||||||
'x-ibm-client-id': CarrefourItems.key['app_id'],
|
'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 req = request(reqopt, (res) => {
|
||||||
|
|
||||||
let chunks = [];
|
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', () => {
|
res.on('end', () => {
|
||||||
let raw = Buffer.concat(chunks)
|
let raw = Buffer.concat(chunks)
|
||||||
try{
|
try{
|
||||||
let json = JSON.parse(raw.toString());
|
let json = JSON.parse(raw.toString());
|
||||||
resolve(json)
|
reprintf(' + carrefour item search'); pad('100%\n')
|
||||||
|
resolve( this.format(json) )
|
||||||
}catch(e){
|
}catch(e){
|
||||||
|
reprintf(' + carrefour item search'); pad('fail\n')
|
||||||
reject(`invalid json response: ${e.message}`);
|
reject(`invalid json response: ${e.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,7 +121,7 @@ export default class CarrefourItems implements Client {
|
||||||
req.end();
|
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 { createServer } from 'http'
|
||||||
import { Socket } from 'net';
|
import { Socket } from 'net';
|
||||||
import Mixer from './mixer'
|
import Mixer from './mixer'
|
||||||
import CarrefourItems from './api/items';
|
import { printf } from './std/printf';
|
||||||
import printf from './fmt/printf';
|
|
||||||
|
|
||||||
|
|
||||||
const mixer: Mixer = new Mixer();
|
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 { parse as parseURL, UrlWithParsedQuery } from "url";
|
||||||
import { ParsedUrlQuery } from "querystring";
|
import { ParsedUrlQuery } from "querystring";
|
||||||
import Client from "./api/interface";
|
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 {
|
export default class Mixer {
|
||||||
|
|
||||||
static valid_urls = ["/"];
|
static valid_urls = ["/"];
|
||||||
|
|
||||||
chain: Client[] = [new CarrefourItems()];
|
chain: Client[] = [new CarrefourItems(), new OpenFoodFacts()];
|
||||||
|
|
||||||
constructor(){
|
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)
|
let urlObj : UrlWithParsedQuery = parseURL(req.url,true)
|
||||||
|
printf('\n -- new client --\n')
|
||||||
|
|
||||||
// reject invalid requests
|
// 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.writeHead(200, { 'Content-Type': 'application/json' })
|
||||||
res.end(JSON.stringify({reason: 'invalid HTTP method; only POST allowed'}))
|
res.end(JSON.stringify( this.format(query_result) ));
|
||||||
return;
|
|
||||||
}else if( Mixer.valid_urls.indexOf(urlObj.pathname) < 0 ){
|
}catch(err){
|
||||||
|
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' })
|
res.writeHead(200, { 'Content-Type': 'application/json' })
|
||||||
res.end(JSON.stringify({reason: `invalid URI; only [${Mixer.valid_urls}] allowed`}))
|
res.end(JSON.stringify({reason: err.message}))
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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++ ){
|
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{
|
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){
|
}catch(e){
|
||||||
printf('fail\n');
|
printf(' ! failure \t\t\t(%s)\n', e.message);
|
||||||
throw e
|
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