Feat global exception (#83)

- add global exceptions
- refactor quest models to reusable parts

other improvements:
- improve error handling
This commit is contained in:
CHEN, TAI-LIN authored and GitHub committed 2023-11-26 22:33:11 +08:00
commit f657f633b4
25 files changed
+233 -203

No files matched your search

+19
View File
@@ -0,0 +1,19 @@
import { Schema } from 'mongoose'
import conditionSchema from './condition.js'
import responseSchema from './response.js'
import hintSchema from './hint.js'
const commandResponderSchema = new Schema({
condition: {
type: conditionSchema,
required: false,
},
responses: {
type: [responseSchema],
required: true,
},
})
commandResponderSchema.add(hintSchema)
export default commandResponderSchema
+28
View File
@@ -0,0 +1,28 @@
import { Schema } from 'mongoose'
const fileConditionSchema = new Schema({
path: {
type: String,
required: true,
},
type: String,
exists: {
type: Boolean,
default: true,
},
})
const conditionSchema = new Schema({
command: [String],
output: [String],
error: [String],
pwd: [String],
files: [fileConditionSchema],
})
conditionSchema.add({
or: [conditionSchema],
not: conditionSchema,
})
export default conditionSchema
+8
View File
@@ -0,0 +1,8 @@
import { Schema } from 'mongoose'
import commandResponderSchema from './commandResponder.js'
const exceptionSchema = new Schema({})
exceptionSchema.add(commandResponderSchema)
export default exceptionSchema
+10
View File
@@ -0,0 +1,10 @@
import { Schema } from 'mongoose'
import exceptionSchema from './exception.js'
import requirementSchema from './requirement.js'
const globalExceptionSchema = new Schema({})
globalExceptionSchema.add(exceptionSchema)
globalExceptionSchema.add(requirementSchema)
export default globalExceptionSchema
+10
View File
@@ -0,0 +1,10 @@
import { Schema } from 'mongoose'
const hintSchema = new Schema({
hints: {
type: [String],
required: true,
},
})
export default hintSchema
+6 -94
View File
@@ -1,98 +1,6 @@
import { model, Schema } from 'mongoose'
const responseSchema = new Schema({
type: {
type: String,
enum: ['narrative', 'dialogue'],
default: 'dialogue',
required: true,
},
content: [String],
speaker: { type: String, default: 'Ada' },
color: String,
})
const fileConditionSchema = new Schema({
path: {
type: String,
required: true,
},
type: String,
exists: {
type: Boolean,
default: true,
},
})
const conditionSchema = new Schema({
command: [String],
output: [String],
error: [String],
pwd: [String],
files: [fileConditionSchema],
})
conditionSchema.add({
or: [conditionSchema],
not: conditionSchema,
})
const exceptionSchema = new Schema({
condition: {
type: conditionSchema,
required: false,
},
catchAll: {
type: Boolean,
required: true,
default: false,
},
responses: {
type: [responseSchema],
required: true,
},
hints: {
type: [String],
required: true,
},
})
const stageSchema = new Schema({
id: {
type: String,
required: true,
},
task: {
type: String,
},
requirements: [
{
type: String,
required: true,
},
],
repeatable: {
type: Boolean,
required: true,
default: false,
},
condition: {
type: conditionSchema,
required: true,
},
responses: {
type: [responseSchema],
required: true,
},
hints: {
type: [String],
required: true,
},
exceptions: {
type: [exceptionSchema],
required: true,
},
})
import stageSchema from './stage.js'
import globalExceptionSchema from './globalException.js'
const Quest = model(
'Quest',
@@ -126,6 +34,10 @@ const Quest = model(
type: [stageSchema],
required: true,
},
exceptions: {
type: [globalExceptionSchema],
required: true,
},
})
)
+12
View File
@@ -0,0 +1,12 @@
import { Schema } from 'mongoose'
const requirementSchema = new Schema({
requirements: [
{
type: String,
required: true,
},
],
})
export default requirementSchema
+15
View File
@@ -0,0 +1,15 @@
import { Schema } from 'mongoose'
const responseSchema = new Schema({
type: {
type: String,
enum: ['narrative', 'dialogue'],
default: 'dialogue',
required: true,
},
content: [String],
speaker: { type: String, default: 'Ada' },
color: String,
})
export default responseSchema
+24
View File
@@ -0,0 +1,24 @@
import { Schema } from 'mongoose'
import commandResponderSchema from './commandResponder.js'
import stageExceptionSchema from './stageException.js'
import requirementSchema from './requirement.js'
const stageSchema = new Schema({
id: {
type: String,
required: true,
},
task: {
type: String,
required: true,
},
exceptions: {
type: [stageExceptionSchema],
required: true,
},
})
stageSchema.add(commandResponderSchema)
stageSchema.add(requirementSchema)
export default stageSchema
+14
View File
@@ -0,0 +1,14 @@
import { Schema } from 'mongoose'
import exceptionSchema from './exception.js'
const stageExceptionSchema = new Schema({
catchAll: {
type: Boolean,
required: true,
default: false,
},
})
stageExceptionSchema.add(exceptionSchema)
export default stageExceptionSchema
+8 -8
View File
@@ -9,14 +9,6 @@ requirements:
- get-started
stages:
- id: warning-01
repeatable: true
condition:
command:
- ' '
hints:
- '!! 請勿使用全形空格 !!'
- id: start
task: 輸入 `echo start` 來開始教程
condition:
@@ -73,3 +65,11 @@ stages:
- 你成功的在終端機上顯示了 `How are you?`。
- 相信你已經了解如何使用 `echo` 指令來顯示文字了。
- 之後如果要在終端機顯示文字,記得可以使用 `echo` 指令。
exceptions:
- repeatable: true
condition:
command:
- ' '
hints:
- '!! 請勿使用全形空格 !!'
-8
View File
@@ -9,14 +9,6 @@ requirements:
- create
stages:
- id: warning-01
repeatable: true
condition:
command:
- ' '
hints:
- '!! 請勿使用全形空格 !!'
- id: start
task: 輸入 `echo start` 來開始
condition:
+1 -8
View File
@@ -10,14 +10,6 @@ requirements:
- read
stages:
- id: warning-01
repeatable: true
condition:
command:
- ' '
hints:
- '!! 請勿使用全形空格 !!'
- id: start
task: 輸入 `echo start` 來開始
condition:
@@ -68,6 +60,7 @@ stages:
- ➤ 輸入 `cat hello.txt` 來查看 `hello.txt` 檔案的內容
- id: cat
task: 使用 `cat` 指令來查看 `hello.txt` 檔案的內容
condition:
command:
- ^cat hello.txt$
+6
View File
@@ -0,0 +1,6 @@
exceptions:
- condition:
command:
- '`'
hints:
- '!! 不需要輸入上下引號 !!'
+1 -9
View File
@@ -10,14 +10,6 @@ requirements:
- get-started
stages:
- id: warning-01
repeatable: true
condition:
command:
- ' '
hints:
- '!! 請勿使用全形空格 !!'
- id: start
task: 輸入 `echo start` 來開始教程
condition:
@@ -115,7 +107,7 @@ stages:
pwd:
- ^/home/commander/folder1$
command:
- ls
- ^ls$
responses:
- content:
- 你現在可以看到 `folder1` 資料夾中有兩的檔案,分別是 `file1.txt` 跟 `file2.txt`。
-16
View File
@@ -9,22 +9,6 @@ instruction: |
請你在終端機中的 `commander:~ $` 後面輸入 `echo start` 並按下 Enter 來開始教程。
stages:
- id: warning-01
repeatable: true
condition:
command:
- ' '
hints:
- '!! 請勿使用全形空格 !!'
- id: warning-02
repeatable: true
condition:
command:
- '`'
hints:
- '!! 不需要輸入上下引號 !!'
- id: start
task: 輸入 `echo start` 來開始教程
condition:
-8
View File
@@ -9,14 +9,6 @@ requirements:
- create
stages:
- id: warning-01
repeatable: true
condition:
command:
- ' '
hints:
- '!! 請勿使用全形空格 !!'
- id: start
task: 輸入 `echo start` 來開始
condition:
-8
View File
@@ -11,14 +11,6 @@ requirements:
- explore
stages:
- id: warning-01
repeatable: true
condition:
command:
- ' '
hints:
- '!! 請勿使用全形空格 !!'
- id: start
task: 輸入 `echo start` 來開始教程
condition:
-8
View File
@@ -9,14 +9,6 @@ requirements:
- path
stages:
- id: warning-01
repeatable: true
condition:
command:
- ' '
hints:
- '!! 請勿使用全形空格 !!'
- id: start
task: 輸入 `echo start` 來開始教程
condition:
-8
View File
@@ -13,14 +13,6 @@ requirements:
- move
stages:
- id: warning-01
repeatable: true
condition:
command:
- ' '
hints:
- '!! 請勿使用全形空格 !!'
- id: start
task: 輸入 `echo start` 來開始
condition:
+33 -16
View File
@@ -56,7 +56,7 @@ export default class CommandHandler extends SessionHandler {
this.session.graph = graph
} catch (e) {
logger.error('Update file graph failed', {
error: e,
error: e.message,
session: this.session.id,
graph: this.session.graph,
discover: this.params.discover,
@@ -122,9 +122,9 @@ export default class CommandHandler extends SessionHandler {
async run() {
try {
this.quest = await Quest.findById(this.session.quest)
const stages = this.getStages()
const stages = this.getUnlockedComponents(this.quest.stages)
if (stages.length === 0) {
logger.warn('stage not found', this.session.tasks)
logger.warn('stage not found', { session: this.session.id })
return {}
}
@@ -136,27 +136,44 @@ export default class CommandHandler extends SessionHandler {
)
const stage = stages.find((_, i) => matches[i])
if (!stage) {
for (const s of stages) {
if (stage) {
const response = this.execute(stage)
return {
stage: stage.id,
end: this.session.status === 'finished',
...response,
}
}
// stage exception
for (const s of stages) {
// eslint-disable-next-line no-await-in-loop
const exception = await this.checkException(s)
if (exception) {
return this.executeException(exception)
}
}
// global exception
if (this.quest.exceptions.length > 0) {
const globalExceptions = this.getUnlockedComponents(
this.quest.exceptions
)
for (const exception of globalExceptions) {
// eslint-disable-next-line no-await-in-loop
const exception = await this.checkException(s)
if (exception) {
if (await this.isMatch(exception.condition)) {
return this.executeException(exception)
}
}
return {}
}
const response = this.execute(stage)
return {
stage: stage.id,
end: this.session.status === 'finished',
...response,
}
return {}
} catch (err) {
logger.error('CommandHandler error', {
error: err,
session: this.session.id,
error: err.message,
session: this.session._id,
quest: this.quest._id,
command: this.commandInput,
})
return {}
+4 -6
View File
@@ -19,16 +19,14 @@ export default class SessionHandler {
return task ? task.completed : false
}
getStages() {
return this.quest.stages.filter(
(stage) =>
stage.requirements.every((r) => this.isCompleted({ id: r })) &&
(stage.repeatable || !this.isCompleted(stage))
getUnlockedComponents(components) {
return components.filter((component) =>
component.requirements.every((r) => this.isCompleted({ id: r }))
)
}
getNewTasks() {
const stages = this.getStages()
const stages = this.getUnlockedComponents(this.quest.stages)
return stages
.filter((s) => s.task)
.filter((s) => !this.session.tasks.some((t) => t.id === s.id))
+6 -1
View File
@@ -38,7 +38,12 @@ async function main() {
await setupTest()
}
await loadAndUpdateQuests()
try {
await loadAndUpdateQuests()
} catch (err) {
logger.error('Failed to load quests', err)
return
}
expiryRemovalScheduler()
const file = await fs.readFile('./swagger.yaml', 'utf8')
+6 -2
View File
@@ -1,9 +1,13 @@
import winston from 'winston'
import winston, { format } from 'winston'
import config from '../config.js'
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
format: format.combine(
format.errors({ stack: true }),
format.timestamp(),
format.json()
),
defaultMeta: { service: 'backend', timestamp: new Date().toISOString() },
transports: [
//
+22 -3
View File
@@ -7,6 +7,15 @@ import logger from './logger.js'
const questDirectory = path.join(process.cwd(), '..', 'quests')
class QuestValidationError extends Error {
constructor(message, questId, error) {
super(message)
this.name = 'QuestValidationError'
this.questId = questId
this.error = error
}
}
async function mapQuests(callback) {
const questNames = await fs.readdir(questDirectory, {
withFileTypes: true,
@@ -23,12 +32,20 @@ async function mapQuests(callback) {
)
}
async function loadGlobalExceptions() {
const exceptionsPath = path.join(questDirectory, 'exceptions.yml')
const body = await fs.readFile(exceptionsPath, 'utf-8')
return yaml.parse(body)
}
export async function loadAndUpdateQuests() {
await Quest.deleteMany({})
const { exceptions } = await loadGlobalExceptions()
console.log(exceptions)
return mapQuests(async (id, questPath) => {
const files = await fs.readdir(questPath)
if (!files.includes('game.yml')) {
throw new Error(`Quest ${id} is missing game.yml`)
throw new QuestValidationError(`Missing game.yml`, id)
}
try {
@@ -44,10 +61,12 @@ export async function loadAndUpdateQuests() {
image,
...questData,
})
quest.exceptions.push(...exceptions)
await quest.save()
} catch (error) {
logger.error(`Error parsing quest ${id}:`, error)
throw error
console.error(error)
logger.error(`Error parsing quest`, { id, error })
throw new QuestValidationError(`Error parsing quest`, id, error)
}
})
}