refactor: remove exceptions from parser

This commit is contained in:
Alexander Drozdov
2023-07-13 22:30:17 +03:00
parent eaa1921ad2
commit 5eec804fa3
7 changed files with 95 additions and 84 deletions

View File

@@ -30,6 +30,7 @@ module.exports = {
'@typescript-eslint/no-floating-promises': 'off',
'@typescript-eslint/no-misused-promises': 'off',
'@typescript-eslint/prefer-reduce-type-parameter': 'off',
'@typescript-eslint/no-invalid-void-type': 'off',
// TODO: refactor IPC and enable
'@typescript-eslint/consistent-type-assertions': 'off'
},

View File

@@ -18,6 +18,7 @@
"fast-deep-equal": "^3.1.3",
"fastest-levenshtein": "^1.0.16",
"luxon": "3.x.x",
"neverthrow": "^6.0.0",
"object-hash": "^3.0.0",
"sockette": "^2.0.6",
"tailwindcss": "3.x.x",

View File

@@ -1,3 +1,4 @@
import { Result, ok, err } from 'neverthrow'
import {
CLIENT_STRINGS as _$,
CLIENT_STRINGS_REF as _$REF,
@@ -20,9 +21,9 @@ type SectionParseResult =
| 'PARSER_SKIPPED'
type ParserFn = (section: string[], item: ParserState) => SectionParseResult
type VirtualParserFn = (item: ParserState) => void
type VirtualParserFn = (item: ParserState) => Result<never, string> | void
export interface ParserState extends ParsedItem {
interface ParserState extends ParsedItem {
name: string
baseType: string | undefined
infoVariants: BaseType[]
@@ -70,11 +71,51 @@ const parsers: Array<ParserFn | { virtual: VirtualParserFn }> = [
{ virtual: calcBasePercentile }
]
export function parseClipboard (clipboard: string) {
const lines = clipboard.split(/\r?\n/)
export function parseClipboard (clipboard: string): Result<ParsedItem, string> {
try {
let sections = itemTextToSections(clipboard)
if (sections[0][2] === _$.CANNOT_USE_ITEM) {
sections[0].pop() // remove CANNOT_USE_ITEM line
sections[1].unshift(...sections[0]) // prepend item class & rarity into second section
sections.shift() // remove first section where CANNOT_USE_ITEM line was
}
const parsed = parseNamePlate(sections[0])
if (!parsed.isOk()) return parsed
sections.shift()
parsed.value.rawText = clipboard
// each section can be parsed at most by one parser
for (const parser of parsers) {
if (typeof parser === 'object') {
const error = parser.virtual(parsed.value)
if (error) return error
continue
}
for (const section of sections) {
const result = parser(section, parsed.value)
if (result === 'SECTION_PARSED') {
sections = sections.filter(s => s !== section)
break
} else if (result === 'PARSER_SKIPPED') {
break
}
}
}
return Object.freeze(parsed)
} catch (e) {
console.log(e)
return err('item.parse_error')
}
}
function itemTextToSections (text: string) {
const lines = text.split(/\r?\n/)
lines.pop()
let sections: string[][] = [[]]
const sections: string[][] = [[]]
lines.reduce((section, line) => {
if (line !== '--------') {
section.push(line)
@@ -85,40 +126,7 @@ export function parseClipboard (clipboard: string) {
return section
}
}, sections[0])
sections = sections.filter(section => section.length)
if (sections[0][2] === _$.CANNOT_USE_ITEM) {
sections[0].pop() // remove CANNOT_USE_ITEM line
sections[1].unshift(...sections[0]) // prepend item class & rarity into second section
sections.shift() // remove first section where CANNOT_USE_ITEM line was
}
const parsed = parseNamePlate(sections[0])
if (!parsed) {
return null
}
sections.shift()
parsed.rawText = clipboard
// each section can be parsed at most by one parser
for (const parser of parsers) {
if (typeof parser === 'object') {
parser.virtual(parsed)
continue
}
for (const section of sections) {
const result = parser(section, parsed)
if (result === 'SECTION_PARSED') {
sections = sections.filter(s => s !== section)
break
} else if (result === 'PARSER_SKIPPED') {
break
}
}
}
return Object.freeze(parsed)
return sections.filter(section => section.length)
}
function normalizeName (item: ParserState) {
@@ -180,7 +188,7 @@ function findInDatabase (item: ParserState) {
info = ITEM_BY_REF('ITEM', item.baseType ?? item.name)
}
if (!info?.length) {
throw new Error('UNKNOWN_ITEM')
return err('item.unknown')
}
if (info[0].unique) {
info = info.filter(info => info.unique!.base === item.baseType)
@@ -267,7 +275,7 @@ function parseNamePlate (section: string[]) {
if (section.length < 3 ||
!section[0].startsWith(_$.ITEM_CLASS) ||
!section[1].startsWith(_$.RARITY)) {
return null
return err('item.parse_error')
}
const item: ParserState = {
@@ -311,10 +319,10 @@ function parseNamePlate (section: string[]) {
item.rarity = ItemRarity.Unique
break
default:
return null
return err('item.unknown')
}
return item
return ok(item)
}
function parseInfluence (section: string[], item: ParsedItem) {

View File

@@ -43,7 +43,7 @@ export default defineComponent({
if (e.target !== 'item-check') return
checkPosition.value = e.position
item.value = parseClipboard(e.clipboard)
item.value = parseClipboard(e.clipboard).unwrapOr(null)
if (item.value) {
wm.show(props.config.wmId)
}

View File

@@ -7,17 +7,17 @@ const POEDB_LANGS = { 'en': 'us', 'ru': 'ru', 'cmn-Hant': 'tw' }
export function registerActions () {
Host.onEvent('MAIN->CLIENT::item-text', (e) => {
if (!['open-wiki', 'open-craft-of-exile', 'open-poedb', 'search-similar'].includes(e.target)) return
const item = parseClipboard(e.clipboard)
if (!item) return
const parsed = parseClipboard(e.clipboard)
if (!parsed.isOk()) return
if (e.target === 'open-wiki') {
openWiki(item)
openWiki(parsed.value)
} else if (e.target === 'open-craft-of-exile') {
openCoE(item)
openCoE(parsed.value)
} else if (e.target === 'open-poedb') {
openPoedb(item)
openPoedb(parsed.value)
} else if (e.target === 'search-similar') {
findSimilarItems(item)
findSimilarItems(parsed.value)
}
})
}

View File

@@ -31,17 +31,17 @@
<background-info />
<check-position-circle v-if="showCheckPos"
:position="checkPosition" style="z-index: -1;" />
<template v-if="item && ('error' in item)">
<template v-if="item?.isErr()">
<ui-error-box class="m-4">
<template #name>{{ t(item.error.name) }}</template>
<p>{{ t(item.error.message) }}</p>
</ui-error-box>
<pre class="bg-gray-900 rounded m-4 overflow-x-hidden p-2">{{ item.rawText }}</pre>
<pre class="bg-gray-900 rounded m-4 overflow-x-hidden p-2">{{ item.error.rawText }}</pre>
</template>
<template v-else>
<unidentified-resolver :item="item" @identify="item = $event" />
<checked-item v-if="isLeagueSelected && item"
:item="item" :advanced-check="advancedCheck" />
<template v-else-if="item?.isOk()">
<unidentified-resolver :item="item.value" @identify="handleIdentification($event)" />
<checked-item v-if="isLeagueSelected"
:item="item.value" :advanced-check="advancedCheck" />
</template>
<div v-if="isBrowserShown" class="bg-gray-900 px-6 py-2 truncate">
<i18n-t keypath="app.toggle_browser_hint" tag="div">
@@ -58,8 +58,8 @@
'flex-row': clickPosition === 'stash',
'flex-row-reverse': clickPosition === 'inventory'
}">
<related-items v-if="item && !('error' in item)" class="pointer-events-auto"
:item="item" :click-position="clickPosition" />
<related-items v-if="item?.isOk()" class="pointer-events-auto"
:item="item.value" :click-position="clickPosition" />
<rate-limiter-state class="pointer-events-auto" />
</div>
</div>
@@ -68,6 +68,7 @@
<script lang="ts">
import { defineComponent, inject, PropType, shallowRef, watch, computed, nextTick, provide } from 'vue'
import { Result, ok, err } from 'neverthrow'
import { useI18n } from 'vue-i18n'
import CheckedItem from './CheckedItem.vue'
import BackgroundInfo from './BackgroundInfo.vue'
@@ -83,13 +84,7 @@ import CheckPositionCircle from './CheckPositionCircle.vue'
import ItemQuickPrice from '@/web/ui/ItemQuickPrice.vue'
import { PriceCheckWidget, WidgetManager } from '../overlay/interfaces'
interface ParseError {
error: {
name: string
message: string
}
rawText: ParsedItem['rawText']
}
type ParseError = { name: string; message: string; rawText: ParsedItem['rawText'] }
export default defineComponent({
components: {
@@ -116,7 +111,7 @@ export default defineComponent({
props.config.wmFlags = ['hide-on-blur', 'skip-menu']
})
const item = shallowRef<ParsedItem | ParseError | null>(null)
const item = shallowRef<null | Result<ParsedItem, ParseError>>(null)
const advancedCheck = shallowRef(false)
const checkPosition = shallowRef({ x: 1, y: 1 })
@@ -149,28 +144,28 @@ export default defineComponent({
wm.show(props.config.wmId)
checkPosition.value = e.position
advancedCheck.value = e.focusOverlay
try {
const parsed = parseClipboard(e.clipboard)
if (parsed != null && (
(parsed.category === ItemCategory.HeistContract && parsed.rarity !== ItemRarity.Unique) ||
(parsed.category === ItemCategory.Sentinel && parsed.rarity !== ItemRarity.Unique)
)) {
throw new Error('UNKNOWN_ITEM')
} else {
item.value = parsed
item.value = parseClipboard(e.clipboard)
.andThen(item => (
(item.category === ItemCategory.HeistContract && item.rarity !== ItemRarity.Unique) ||
(item.category === ItemCategory.Sentinel && item.rarity !== ItemRarity.Unique))
? err('item.unknown')
: ok(item))
.mapErr(err => ({
name: `${err}`,
message: `${err}_help`,
rawText: e.clipboard
}))
if (item.value.isOk()) {
queuePricesFetch()
}
} catch (err: unknown) {
const strings = (err instanceof Error && err.message === 'UNKNOWN_ITEM')
? 'item.unknown'
: 'item.parse_error'
item.value = {
error: { name: `${strings}`, message: `${strings}_help` },
rawText: e.clipboard
}
}
})
function handleIdentification (identified: ParsedItem) {
item.value = ok(identified)
}
MainProcess.onEvent('MAIN->OVERLAY::hide-exclusive-widget', () => {
wm.hide(props.config.wmId)
})
@@ -253,6 +248,7 @@ export default defineComponent({
checkPosition,
item,
advancedCheck,
handleIdentification,
overlayKey,
isLeagueSelected,
openLeagueSelection

View File

@@ -1693,6 +1693,11 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
neverthrow@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/neverthrow/-/neverthrow-6.0.0.tgz#bacd7661cade296ccc5c35760bb3b679214155b6"
integrity sha512-kPZKRs4VkdloCGQXPoP84q4sT/1Z+lYM61AXyV8wWa2hnuo5KpPBF2S3crSFnMrOgUISmEBP8Vo/ngGZX60NhA==
node-releases@^2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503"