diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..2be230e --- /dev/null +++ b/.eslintrc @@ -0,0 +1,21 @@ +{ + "env": { + "es6": true, + "node": true, + "jest/globals": true + }, + "plugins": [ + "jest" + ], + "extends": [ + "airbnb-base" + ], + "globals": { + "Atomics": "readonly", + "SharedArrayBuffer": "readonly" + }, + "parserOptions": { + "ecmaVersion": 2018, + "sourceType": "module" + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 621038f..abb508c 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,6 @@ typings/ # dotenv environment variables file .env + +# Yarn Lock file +yarn.lock diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..5ad9124 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,188 @@ +// For a detailed explanation regarding each configuration property, visit: +// https://jestjs.io/docs/en/configuration.html + +module.exports = { + // All imported modules in your tests should be mocked automatically + // automock: false, + + // Stop running tests after `n` failures + // bail: 0, + + // Respect "browser" field in package.json when resolving modules + // browser: false, + + // The directory where Jest should store its cached dependency information + // cacheDirectory: "/private/var/folders/qd/35f__mv96m194f8hr0ny5gb80000gp/T/jest_dy", + + // Automatically clear mock calls and instances between every test + clearMocks: true, + + // Indicates whether the coverage information should be collected while executing the test + // collectCoverage: false, + + // An array of glob patterns indicating a set of files for which coverage information should be collected + // collectCoverageFrom: null, + + // The directory where Jest should output its coverage files + coverageDirectory: 'coverage', + + // An array of regexp pattern strings used to skip coverage collection + // coveragePathIgnorePatterns: [ + // "/node_modules/" + // ], + + // A list of reporter names that Jest uses when writing coverage reports + // coverageReporters: [ + // "json", + // "text", + // "lcov", + // "clover" + // ], + + // An object that configures minimum threshold enforcement for coverage results + // coverageThreshold: null, + + // A path to a custom dependency extractor + // dependencyExtractor: null, + + // Make calling deprecated APIs throw helpful error messages + // errorOnDeprecated: false, + + // Force coverage collection from ignored files using an array of glob patterns + // forceCoverageMatch: [], + + // A path to a module which exports an async function that is triggered once before all test suites + // globalSetup: null, + + // A path to a module which exports an async function that is triggered once after all test suites + // globalTeardown: null, + + // A set of global variables that need to be available in all test environments + // globals: {}, + + // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. + // maxWorkers: "50%", + + // An array of directory names to be searched recursively up from the requiring module's location + // moduleDirectories: [ + // "node_modules" + // ], + + // An array of file extensions your modules use + // moduleFileExtensions: [ + // "js", + // "json", + // "jsx", + // "ts", + // "tsx", + // "node" + // ], + + // A map from regular expressions to module names that allow to stub out resources with a single module + // moduleNameMapper: {}, + + // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader + // modulePathIgnorePatterns: [], + + // Activates notifications for test results + // notify: false, + + // An enum that specifies notification mode. Requires { notify: true } + // notifyMode: "failure-change", + + // A preset that is used as a base for Jest's configuration + // preset: null, + + // Run tests from one or more projects + // projects: null, + + // Use this configuration option to add custom reporters to Jest + // reporters: undefined, + + // Automatically reset mock state between every test + // resetMocks: false, + + // Reset the module registry before running each individual test + // resetModules: false, + + // A path to a custom resolver + // resolver: null, + + // Automatically restore mock state between every test + // restoreMocks: false, + + // The root directory that Jest should scan for tests and modules within + // rootDir: null, + + // A list of paths to directories that Jest should use to search for files in + // roots: [ + // "" + // ], + + // Allows you to use a custom runner instead of Jest's default test runner + // runner: "jest-runner", + + // The paths to modules that run some code to configure or set up the testing environment before each test + // setupFiles: [], + + // A list of paths to modules that run some code to configure or set up the testing framework before each test + // setupFilesAfterEnv: [], + + // A list of paths to snapshot serializer modules Jest should use for snapshot testing + // snapshotSerializers: [], + + // The test environment that will be used for testing + testEnvironment: 'node', + + // Options that will be passed to the testEnvironment + // testEnvironmentOptions: {}, + + // Adds a location field to test results + // testLocationInResults: false, + + // The glob patterns Jest uses to detect test files + // testMatch: [ + // "**/__tests__/**/*.[jt]s?(x)", + // "**/?(*.)+(spec|test).[tj]s?(x)" + // ], + + // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped + // testPathIgnorePatterns: [ + // "/node_modules/" + // ], + + // The regexp pattern or array of patterns that Jest uses to detect test files + // testRegex: [], + + // This option allows the use of a custom results processor + // testResultsProcessor: null, + + // This option allows use of a custom test runner + // testRunner: "jasmine2", + + // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href + // testURL: "http://localhost", + + // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" + // timers: "real", + + // A map from regular expressions to paths to transformers + // transform: null, + + // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation + // transformIgnorePatterns: [ + // "/node_modules/" + // ], + + // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them + // unmockedModulePathPatterns: undefined, + + // Indicates whether each individual test should be reported during the run + // verbose: null, + + // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode + // watchPathIgnorePatterns: [], + + // Whether to use watchman for file crawling + // watchman: true, +}; diff --git a/lib/qwant-api.js b/lib/qwant-api.js index b49bd63..904ebc2 100644 --- a/lib/qwant-api.js +++ b/lib/qwant-api.js @@ -1,149 +1,138 @@ -"use strict"; -let request = require("request"); -let langs = require("./langs"); +const request = require('request'); +const langs = require('./langs'); -//////////////////////////////// -//----------------------------// +// ////////////////////////////// +// ----------------------------// // Copyright (c) 2018 NullDev // -//----------------------------// -//////////////////////////////// - -const noop = () => {}; -const userAgentString = "Mozilla/5.0 (Windows NT 6.3; WOW64; rv:56.0) Gecko/20100101 Firefox/56.0"; - -let fields = [ - "web", - "images", - "news", - "social", - "videos", - "music" +// ----------------------------// +// ////////////////////////////// + +const noop = () => { }; + +const userAgentString = 'Mozilla/5.0 (Windows NT 6.3; WOW64; rv:56.0) Gecko/20100101 Firefox/56.0'; + +const fields = [ + 'web', + 'images', + 'news', + 'social', + 'videos', + 'music', ]; -let ParseBody = class { - web(body){ - let cache = body.data.result.items; - for (var i in cache) for (var key in cache[i]) delete cache[i]["_id"]; - return cache; - } - images(body){ - return body; - } +class ParseBody { + static web({ result }) { + return result.items.map(({ _id, ...item }) => item); + } - news(body){ - return body; - } + static images({ result }) { + return result.items; + } - social(body){ - return body; - } + static news({ result }) { + return result.items; + } - videos(body){ - return body; - } + static social({ result }) { + return result.items; + } - music(body){ - return body; - } -}; + static videos({ result }) { + return result.items; + } + + static music({ result }) { + return result.items; + } +} function getURL(keyword, count, offset, query, lang) { return `https://api.qwant.com/api/search/${keyword}?count=${count}&q=${query}&t=${query}&f=&offset=${offset}&locale=${lang}&uiv=4`; } -function isInt(n){ return n % 1 === 0; } +function qwantApiError(msg) { return new Error(`qwant-api error: ${msg}`); } -function errLog(msg){ console.log("\nqwant-api error: " + msg + "\n"); } -function isJSON(str){ - try { - let o = JSON.parse(str); - if (o && typeof o === "object") return o; - } - catch (e) { noop(); } - return false; +function isJSON(str) { + try { + const o = JSON.parse(str); + if (o && typeof o === 'object') return o; + } catch (e) { noop(); } + return false; } -function performSearch(query, uri, callback){ - let options = { - url: uri, - method : "GET", - headers: { "User-Agent": userAgentString } - }; - - request(options, function(err, response, body){ - if (err) return errLog(err); - if (!isJSON(body) || body == null) return errLog("The API returned an invalid response: " + body); - - let finalRes = JSON.parse(body); - - if ((finalRes.status).toString().toLowerCase() != "success") return errLog("The API returned the error code '" + finalRes.error + "'"); - - let data = null; - let parseBody = new ParseBody(); - - switch(query){ - case "web": { - data = parseBody.web(finalRes); - break; - } - case "images": { - data = parseBody.images(finalRes); - break; - } - case "news": { - data = parseBody.news(finalRes); - break; - } - case "social": { - data = parseBody.social(finalRes); - break; - } - case "videos": { - data = parseBody.videos(finalRes); - break; - } - case "music": { - data = parseBody.music(finalRes); - break; - } - default: return errLog("Something unexpected happend"); - } - - return callback(null, data); +async function performSearch(query, uri) { + const options = { + url: uri, + method: 'GET', + headers: { 'User-Agent': userAgentString }, + }; + + return new Promise((resolve, reject) => { + request(options, (err, response, body) => { + if (err) { + return reject(err); + } + + if (!isJSON(body) || body == null) return reject(qwantApiError(`The API returned an invalid response: ${body}`)); + + const { error, status, data } = JSON.parse(body); + + if (status && status.toString().toLowerCase() !== 'success') { + return reject(qwantApiError(`The API returned the error code "${error}"`)); + } + + try { + const result = ParseBody[query](data); + return resolve(result); + } catch (parseErr) { + return reject(qwantApiError('Something unexpected happend')); + } }); + }); } -let qwant = { - search: function (query, options, callback) { - if (!query || query == null) return errLog("'category' is required"); +const qwant = { + async search(query, options) { + return new Promise((resolve, reject) => { + if (!query || query == null) { return reject(qwantApiError("'category' is required")); } - query = query.toLowerCase(); + const userQuery = query.toLowerCase(); - if (!fields.includes(query)) return errLog(query + "' is not a valid category."); - if (!options || !options.query) return errLog("No search query specified."); + if (!fields.includes(userQuery)) { + return reject(qwantApiError(`${userQuery}' is not a valid category.`)); + } - let count = (options.count ? options.count : 1); - let offst = (options.offset ? options.offset : 0); - if (isNaN(count)) return errLog("'count' needs to be number"); - if (isNaN(offst)) return errLog("'offset' needs to be number"); + if (!options || !options.query) { return qwantApiError('No search query specified.'); } - if (!isInt(count)) return errLog("'count' needs to be a whole number (integer)"); - if (!isInt(offst)) return errLog("'offset' needs to be a whole number (integer)"); + const count = (options.count ? options.count : 1); + const offst = (options.offset ? options.offset : 0); - let sq = encodeURIComponent(options.query); - let langKey = (options.language ? options.language : "english").toLowerCase(); + if (Number.isNaN(count)) return reject(qwantApiError("'count' needs to be number")); + if (Number.isNaN(offst)) return reject(qwantApiError("'offset' needs to be number")); - if (!(langKey in langs)) return errLog("The language '" + langKey + "' does not Exist."); + if (!Number.isInteger(count)) return reject(qwantApiError("'count' needs to be a whole number (integer)")); + if (!Number.isInteger(offst)) return reject(qwantApiError("'offset' needs to be a whole number (integer)")); - let lang = langs[langKey]; - let queryUrl = getURL(query, count, offst, sq, lang); + const sq = encodeURIComponent(options.query); + const langKey = (options.language ? options.language : 'english').toLowerCase(); - performSearch(query, queryUrl, callback); - }, + if (!(langKey in langs)) return reject(qwantApiError(`The language '${langKey}' does not Exist.`)); - getLanguages: function(code){ return (code ? langs : Object.keys(langs)); } -} + const lang = langs[langKey]; + const queryURL = getURL(query, count, offst, sq, lang); + + try { + const search = performSearch(query, queryURL); + return resolve(search); + } catch (err) { + return reject(err); + } + }); + }, + + getLanguages(code) { return (code ? langs : Object.keys(langs)); }, +}; module.exports = qwant; diff --git a/package.json b/package.json index 5ca1e76..44a6c2e 100644 --- a/package.json +++ b/package.json @@ -1,33 +1,43 @@ { - "name": "qwant-api", - "version": "0.5.0", - "description": "Unofficial API Wrapper for QWANT", - "main": "index.js", - "dependencies": { - "request": "^2.83.0" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/NLDev/qwant-api.git" - }, - "maintainers": [ - { - "name": "NullDev", - "email": "admin@nulldev.org", - "web": "nulldev.org" - } - ], - "keywords": [ - "qwant", - "wrapper", - "API", - "REST", - "unofficial" - ], - "author": "NullDev", - "license": "Apache-2.0", - "bugs": { - "url": "https://github.com/NLDev/qwant-api/issues" - }, - "homepage": "https://github.com/NLDev/qwant-api#readme" + "name": "qwant-api", + "version": "0.5.0", + "description": "Unofficial API Wrapper for QWANT", + "main": "index.js", + "dependencies": { + "request": "^2.83.0" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/NLDev/qwant-api.git" + }, + "maintainers": [ + { + "name": "NullDev", + "email": "admin@nulldev.org", + "web": "nulldev.org" + } + ], + "keywords": [ + "qwant", + "wrapper", + "API", + "REST", + "unofficial" + ], + "author": "NullDev", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/NLDev/qwant-api/issues" + }, + "homepage": "https://github.com/NLDev/qwant-api#readme", + "devDependencies": { + "eslint": "^6.5.1", + "eslint-config-airbnb-base": "^14.0.0", + "eslint-plugin-import": "^2.18.2", + "eslint-plugin-jest": "^22.17.0", + "jest": "^24.9.0" + }, + "scripts": { + "test": "jest" + } } diff --git a/test/qwant-api.test.js b/test/qwant-api.test.js new file mode 100644 index 0000000..4ddd68b --- /dev/null +++ b/test/qwant-api.test.js @@ -0,0 +1,35 @@ +const qwant = require('../lib/qwant-api'); + +describe('Test qwant-api lib', () => { + it('search web for nodejs', async () => { + expect.assertions(1); + const result = await qwant.search('web', { query: 'nodejs' }); + expect(result).toHaveLength(1); + }); + + it('search images : cat', async () => { + const result = await qwant.search('images', { query: 'cat' }); + expect(result).toHaveLength(1); + }); + + it('search news : bitcoin', async () => { + const result = await qwant.search('news', { query: 'bitcoin' }); + expect(result).toHaveLength(1); + }); + + it('search social : bill gates', async () => { + const result = await qwant.search('social', { query: 'bill gates' }); + expect(result).toHaveLength(1); + }); + + it('search videos : memes', async () => { + const result = await qwant.search('videos', { query: 'memes' }); + expect(result).toHaveLength(1); + }); + + /* + it('search social : bill gates', async () => { + const result = await qwant.search('music', { query: 'varien' }); + expect(result).toBe(); + }); */ +}); diff --git a/test/test.js b/test/test.js deleted file mode 100644 index b015943..0000000 --- a/test/test.js +++ /dev/null @@ -1,32 +0,0 @@ -"use strict"; -var qwant = require("../lib/qwant-api"); - -qwant.search("web", { query: "nodejs" }, function(err, data){ - if (err) return console.log(err); - console.log("Web Search: \n\n" + JSON.stringify(data, null, 4) + "\n\n----\n"); -}); - -qwant.search("images", { query: "cat" }, function(err, data){ - if (err) return console.log(err); - console.log("Image Search: \n\n" + JSON.stringify(data, null, 4) + "\n\n----\n"); -}); - -qwant.search("news", { query: "bitcoin" }, function(err, data){ - if (err) return console.log(err); - console.log("News Search: \n\n" + JSON.stringify(data, null, 4) + "\n\n----\n"); -}); - -qwant.search("social", { query: "bill gates" }, function(err, data){ - if (err) return console.log(err); - console.log("Social Search: \n\n" + JSON.stringify(data, null, 4) + "\n\n----\n"); -}); - -qwant.search("videos", { query: "memes" }, function(err, data){ - if (err) return console.log(err); - console.log("Videos Search: \n\n" + JSON.stringify(data, null, 4) + "\n\n----\n"); -}); - -qwant.search("music", { query: "varien" }, function(err, data){ - if (err) return console.log(err); - console.log("Music Search: \n\n" + JSON.stringify(data, null, 4) + "\n\n----\n"); -});