import isearr from 'wsemi/src/isearr.mjs'
import isestr from 'wsemi/src/isestr.mjs'
import { isValidAdapter } from './adapterContract.mjs'
//以RegExp比對網址, 且不留下狀態
//帶g或y旗標之RegExp其test會推進lastIndex, 使同一網址連查結果交替;
//此處改以去除g與y之複本比對, 既避免狀態殘留, 亦不竄改呼叫端(或內建清單)持有之RegExp實例
function _testRegExp(re, url) {
if (!re.global && !re.sticky) {
return re.test(url)
}
return new RegExp(re.source, re.flags.replace(/[gy]/g, '')).test(url)
}
/**
* 由adapter清單中找出第一個命中網址者
*
* adapter之形狀與合法性判準以src/adapterContract.mjs為唯一事實來源,本函數只負責挑選。
*
* 依序試各adapter,第一個命中者勝出;條目不合法(非物件、缺id、match型別不符、缺parse)一律略過。
* match執行拋錯時回傳type='error'而非視為未命中——呼叫端註冊了adapter即代表選定該解析階段,
* 若靜默改用預設解析器,等於讓呼叫端在不知情下經歷未選擇的處理管線階段,故一律顯性回報。
* match之回傳值一律await,故async match可正常運作,其reject亦被同一錯誤邊界攔下
*
* @param {String} url 輸入待比對網址字串
* @param {Array} adapters 輸入adapter物件陣列,非陣列時視為空陣列
* @returns {Promise} 回傳Promise,resolve回傳結果物件,命中時為{type:'hit',adapter,ctx},其中ctx為match函數之回傳值(RegExp或回傳true時為null);未命中時為{type:'miss'};match執行拋錯時為{type:'error',id,message},本函數不會reject
* @example
*
* import findAdapter from './src/findAdapter.mjs'
*
* let adapters = [
* { id: 'a', match: /^https?:\/\/a\.com\//, parse: () => ({ success: true }) },
* { id: 'b', match: (u) => { let m = u.match(/\/ar-(\w+)/); return m ? { id: m[1] } : null }, parse: () => ({ success: true }) },
* ]
*
* console.log(findAdapter('https://a.com/x', adapters).adapter.id)
* // => 'a'
*
* console.log(findAdapter('https://b.com/ar-AA1X', adapters).ctx)
* // => { id: 'AA1X' }
*
* console.log(findAdapter('https://c.com/', adapters))
* // => { type: 'miss' }
*
*/
async function findAdapter(url, adapters) {
//check
if (!isestr(url)) {
return { type: 'miss' }
}
if (!isearr(adapters)) {
return { type: 'miss' }
}
for (let adapter of adapters) {
if (!isValidAdapter(adapter)) {
continue
}
//match之執行一律納入同一錯誤邊界
//RegExp之test亦可能拋錯(被覆寫、subclass自訂exec), 與函數式match同等處理
let hit = null
try {
if (adapter.match instanceof RegExp) {
hit = _testRegExp(adapter.match, url)
}
else {
//await可同時涵蓋同步拋錯與async reject, 避免async match產生unhandledRejection,
//亦避免Promise物件被當成truthy而誤判為命中並成為ctx
hit = await adapter.match(url)
}
}
catch (err) {
return { type: 'error', id: adapter.id, message: 'adapter ' + adapter.id + ' match error: ' + (err?.message || String(err)) }
}
if (hit) {
return { type: 'hit', adapter, ctx: hit === true ? null : hit }
}
}
return { type: 'miss' }
}
export default findAdapter