From d30c4458685297b9ea7e681afe49c223d84a3458 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Wed, 24 Oct 2018 17:27:24 +0200 Subject: [PATCH] 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 --- src/api/interface.ts | 6 +- src/api/items.ts | 79 ++++++++++++++++++++------ src/api/off.ts | 129 +++++++++++++++++++++++++++++++++++++++++++ src/fmt/printf.ts | 7 --- src/main.ts | 3 +- src/mixer.ts | 81 ++++++++++++++++++--------- src/std/extract.ts | 18 ++++++ src/std/printf.ts | 29 ++++++++++ 8 files changed, 299 insertions(+), 53 deletions(-) create mode 100644 src/api/off.ts delete mode 100644 src/fmt/printf.ts create mode 100644 src/std/extract.ts create mode 100644 src/std/printf.ts diff --git a/src/api/interface.ts b/src/api/interface.ts index 3faedd9..1e26b1f 100644 --- a/src/api/interface.ts +++ b/src/api/interface.ts @@ -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; + get_all() : Promise[]; + + get(i: number) : Promise; } \ No newline at end of file diff --git a/src/api/items.ts b/src/api/items.ts index 19e9a0b..ca89acf 100644 --- a/src/api/items.ts +++ b/src/api/items.ts @@ -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{ + 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{ return null; } + + get_all() : Promise[]{ - // check query - if( typeof query != 'string' ) - return new Promise((_,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( (resolve, reject) => { + return [new Promise( (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(); - }); + }) ] } diff --git a/src/api/off.ts b/src/api/off.ts new file mode 100644 index 0000000..e940b76 --- /dev/null +++ b/src/api/off.ts @@ -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{ + + // 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( (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[]{ + + printf(' + open food facts search'); + + /* FOR EACH RESULT */ + let promises: Promise[] = []; + 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 + + } + + +} \ No newline at end of file diff --git a/src/fmt/printf.ts b/src/fmt/printf.ts deleted file mode 100644 index 878b610..0000000 --- a/src/fmt/printf.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { format } from "util"; - -export default function printf(fmt:string, ...options:any){ - - process.stdout.write( format(fmt, ...options) ); - -} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 6d4dcf0..2e0f1dd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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(); diff --git a/src/mixer.ts b/src/mixer.ts index 4a20ab8..e03a58f 100644 --- a/src/mixer.ts +++ b/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') + 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 + } + } + } \ No newline at end of file diff --git a/src/std/extract.ts b/src/std/extract.ts new file mode 100644 index 0000000..0f9a80a --- /dev/null +++ b/src/std/extract.ts @@ -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 + +} \ No newline at end of file diff --git a/src/std/printf.ts b/src/std/printf.ts new file mode 100644 index 0000000..3079f0a --- /dev/null +++ b/src/std/printf.ts @@ -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) +} \ No newline at end of file