commit d7711ba95fc445e4935919bdb0e3cedb3d9b3873 Author: didier amyot Date: Thu Oct 19 11:48:30 2023 -0400 implemented homework diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f5dadd --- /dev/null +++ b/.gitignore @@ -0,0 +1,131 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.*.idea +.idea diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/src/buttonclient.ts b/src/buttonclient.ts new file mode 100644 index 0000000..754806b --- /dev/null +++ b/src/buttonclient.ts @@ -0,0 +1,52 @@ +import { LiftRequest} from "./shared.ts"; + +class ButtonClient{ + + private readonly host: string; + + constructor(host: string) { + this.host = host; + } + + private async sendPost(url: string, payload: unknown): Promise{ + return await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }); + } + + async listLiftRequests(): Promise { + const resp = await fetch(`${this.host}/api/v1/lift-requests`); + const ret = await resp.json(); + return ret as LiftRequest[] ; + } + + async createLiftRequest(direction: string, level: number): Promise { + const payload = { + direction: direction, + level: level, + } + const resp = await this.sendPost(`${this.host}/api/v1/lift-requests`, payload); + return 199 < resp.status && resp.status < 300; + } + +} + +async function experiment(): Promise{ + const client = new ButtonClient("http://localhost:3000"); + console.log(await client.listLiftRequests()); + console.log(await client.createLiftRequest("UP", -1)) + console.log(await client.createLiftRequest("UP", -1)) + console.log(await client.listLiftRequests()); + console.log(await client.createLiftRequest("UP", 10)) + console.log(await client.createLiftRequest("UP", 10)) + console.log(await client.listLiftRequests()); + + +} + + +await experiment() \ No newline at end of file diff --git a/src/liftclient.ts b/src/liftclient.ts new file mode 100644 index 0000000..f56f6fa --- /dev/null +++ b/src/liftclient.ts @@ -0,0 +1,91 @@ +import {Lift, LiftFilterCriteron, SmallLiftRepresentation} from "./shared.ts"; +import {h} from "https://esm.sh/stable/preact@10.8.1/denonext/preact.mjs"; + +class LiftClient{ + + private readonly host: string; + + constructor(host: string) { + this.host = host; + } + + async listElevators(): Promise { + const resp = await fetch(`${this.host}/api/v1/lifts`); + const ret = await resp.json(); + return ret.lifts as SmallLiftRepresentation[] ; + } + + async filterLifts(criteron: LiftFilterCriteron): Promise { + const searchParams = new URLSearchParams(criteron); + const resp = await fetch(`${this.host}/api/v1/lifts?${searchParams}`); + const ret = await resp.json(); + return ret.lifts as SmallLiftRepresentation[]; + } + + async getLiftById(id: number): Promise { + const resp = await fetch(`${this.host}/api/v1/lifts/${id}`); + if(resp.status === 404){ + return null; + } + return (await resp.json()) as Lift; + } + + private async sendPost(url: string, payload: unknown): Promise{ + return await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }); + } + + async moveLift(liftId: number, newLevel: number): Promise { + const payload = { + type: 'lift-move', + level: newLevel, + } + const resp = await this.sendPost(`${this.host}/api/v1/lifts/${liftId}`, payload); + return 199 < resp.status && resp.status < 300; + } + + async openLiftDoor(liftId: number): Promise { + const lift = await this.getLiftById(liftId); + if(lift == null){ + return false; + } + const payload = { + type: 'door-open', + level: lift.level, + } + const resp = await this.sendPost(`${this.host}/api/v1/lifts/${liftId}`, payload); + return 199 < resp.status && resp.status < 300; + } + + async setDestination(liftId: number, newDestination: number): Promise { + const payload = { + type: 'add-destination', + destination: newDestination, + } + const resp = await this.sendPost(`${this.host}/api/v1/lifts/${liftId}`, payload); + return 199 < resp.status && resp.status < 300; + } +} + +async function experiment(): Promise{ + const client = new LiftClient("http://localhost:3000"); + console.log(await client.getLiftById(2)); + console.log(await client.setDestination(2, -1)) + console.log(await client.setDestination(2, 11)) + console.log(await client.setDestination(2, 16)) + console.log(await client.setDestination(2, 14)) + console.log(await client.setDestination(2, -1)) + + + console.log(await client.getLiftById(2)); + console.log(await client.openLiftDoor(2)) + console.log(await client.getLiftById(2)); +} + + +await experiment() \ No newline at end of file diff --git a/src/liftserver.ts b/src/liftserver.ts new file mode 100644 index 0000000..0e489b9 --- /dev/null +++ b/src/liftserver.ts @@ -0,0 +1,299 @@ +// @deno-types="npm:@types/express@4.17.20" +import express, {Response, Request} from "npm:express@4.18.2"; +import bodyParser from "npm:body-parser@1.20.2"; +import {Lift, LiftFilterCriteron, LiftRequest, SmallLiftRepresentation} from "./shared.ts"; +import c from "https://esm.sh/v132/@babel/runtime@7.22.15/denonext/helpers/getPrototypeOf.js"; + +const app = express() +app.use(bodyParser.json()); + +const port = 3000; + +type LiftSearchRequestParams = { + floor?: string, + min_floor?: string, + max_floor?: string, + direction?: string, +} + +type LiftAction = { + type: string, + level?: number, + destination?: number, +} + +class ButtonService{ + private readonly liftRequests : LiftRequest[] = []; + + getLiftRequests(): LiftRequest[]{ + return this.liftRequests; + } + + addLiftRequest(liftRequest: LiftRequest): boolean{ + for(const lr of this.liftRequests){ + if(lr.level === liftRequest.level && lr.direction === liftRequest.direction){ + return false; + } + } + this.liftRequests.push(liftRequest); + return true; + } +} + +class LiftService { + + private readonly lifts: Lift[]; + + constructor(lifts: Lift[]) { + this.lifts = lifts; + } + + getAllLifts(): Lift[] { + return this.lifts; + } + + private computeDirection(lift: Lift): string{ + if(lift.destinations.length === 0){ + return "IDLE"; + } + const nextDestination = lift.destinations[0]; + if(nextDestination === lift.level){ + return lift.direction; + } + if(nextDestination > lift.level){ + return "UP"; + } + return "DOWN"; + } + + private isBetween(x: number, min_: number, max_: number): boolean{ + return min_ < x && x < max_; + } + + private addDestination(destinations: number[], currentLevel: number, newDestination: number){ + if(destinations.length === 0){ + return [newDestination]; + } + if(destinations.includes(newDestination)){ + return destinations; + } + + const firstDestination = destinations[0]; + if(this.isBetween(newDestination, currentLevel, firstDestination)){ + return [newDestination].concat(destinations); + + } + for(let i = 0; i < destinations.length - 1; i++){ + const first = destinations[i]; + const second = destinations[i + 1]; + if(this.isBetween(newDestination, first, second)){ + const firstPart = destinations.slice(0, i + 1); + const secondPart = destinations.slice(i + 1); + return firstPart.concat([newDestination]).concat(secondPart); + } + } + return destinations.concat([newDestination]); + + } + + processAddDestination(liftId: number, level: number): boolean { + const lift = this.getElevatorById(liftId); + if(!lift){ + return false; + } + lift.destinations = this.addDestination(lift.destinations, lift.level, level); + lift.direction = this.computeDirection(lift); + return true; + } + + processDoorOpen(liftId: number, level: number): boolean { + const lift = this.getElevatorById(liftId); + if(!lift){ + return false; + } + lift.destinations = this.removeFromArray(lift.destinations, level); + lift.direction = this.computeDirection(lift); + return true; + } + removeFromArray(lst: number[], toRemove: number): number[] { + const ret: number[] = []; + for(const e of lst){ + if(e !== toRemove){ + ret.push(e); + } + } + return ret + } + + processLiftMovement(liftId: number, level: number): boolean { + const lift = this.getElevatorById(liftId); + if(!lift){ + return false; + } + lift.level = level; + return true; + } + + private matches(lift: Lift, liftFilterCriteria: LiftFilterCriteron) { + if (liftFilterCriteria.floor !== undefined && lift.level != liftFilterCriteria.floor) { + return false; + } + + if (liftFilterCriteria.min_floor !== undefined && lift.level < liftFilterCriteria.min_floor) { + return false; + } + + if (liftFilterCriteria.max_floor !== undefined && lift.level > liftFilterCriteria.max_floor) { + return false; + } + + if (liftFilterCriteria.direction !== undefined && lift.direction !== liftFilterCriteria.direction) { + return false; + } + + return true; + } + + getAllListsMatching(liftFilterCriteria: LiftFilterCriteron): Lift[] { + const ret: Lift[] = []; + for (const lift of this.lifts) { + if (this.matches(lift, liftFilterCriteria)) { + ret.push(lift); + } + } + return ret; + } + + getElevatorById(id: number): Lift | null { + for (const lift of this.lifts) { + if (lift.id === id) { + return lift; + } + } + return null; + } +} + + +function createLiftService(): LiftService { + const defaultLifts: Lift[] = [ + {id: 1, level: 12, direction: 'IDLE', destinations: []}, + {id: 2, level: -1, direction: 'IDLE', destinations: []}, + {id: 3, level: 5, direction: 'IDLE', destinations: []}, + {id: 4, level: 17, direction: 'IDLE', destinations: []}, + ]; + return new LiftService(defaultLifts); +} + +function toSmallRepresentation(lifts: Lift[]): SmallLiftRepresentation[] { + const ret: SmallLiftRepresentation[] = []; + for (const lift of lifts) { + ret.push({id: lift.id, level: lift.level}); + } + return ret; + +} + +const liftService = createLiftService(); + +const buttonService = new ButtonService(); + +function send404(res: Response): void { + res.status(404); + res.send({}); +} + +function send400(res: Response): void { + res.status(400); + res.send({}); +} + +app.get('/api/v1/lifts', (req: Request, res: Response): void => { + const query = req.query as LiftSearchRequestParams; + const criterion = liftSearchRequestParamsToLiftFilterCriteria(query); + const ret = liftService.getAllListsMatching(criterion); + res.send({lifts: toSmallRepresentation(ret)}) +}); + +app.get('/api/v1/lifts/:id', (req: Request<{ id: string }>, res: Response): void => { + const id = req.params.id; + const ret = liftService.getElevatorById(parseInt(id)); + if (ret == null) { + return send404(res); + } + res.send(ret) +}); + +app.post('/api/v1/lifts/:id', (req: Request<{ id: string }>, res: Response): void => { + const id = parseInt(req.params.id); + const ret = liftService.getElevatorById(id); + if (ret == null) { + return send404(res); + } + const action = req.body as LiftAction | null; + if(!action || !isValidAction(action)){ + return send400(res) + } + if(action.type === 'lift-move'){ + liftService.processLiftMovement(id, action.level!); + } + if(action.type === 'door-open'){ + liftService.processDoorOpen(id, action.level!); + } + if(action.type === 'add-destination'){ + liftService.processAddDestination(id, action.destination!); + } + + res.send(ret) +}); + +app.get('/api/v1/lift-requests', (req: Request, res: Response): void => { + res.send(buttonService.getLiftRequests()); +}); + +app.post('/api/v1/lift-requests', (req: Request, res: Response): void => { + const changed = buttonService.addLiftRequest(req.body as LiftRequest); + res.send({changed: changed}); +}); + +app.listen(port, () => { + console.log(`Example app listening on port ${port}`) +}) + +function liftSearchRequestParamsToLiftFilterCriteria(query: LiftSearchRequestParams): LiftFilterCriteron { + const ret: LiftFilterCriteron = {}; + if (query.direction !== undefined) { + ret.direction = query.direction; + } + if (query.floor !== undefined) { + ret.floor = parseInt(query.floor); + } + if (query.max_floor !== undefined) { + ret.max_floor = parseInt(query.max_floor); + } + if (query.min_floor !== undefined) { + ret.min_floor = parseInt(query.min_floor); + } + return ret; +} + +function isLevel(x: any): boolean{ + const allLevels = [ + -2, -1, 1, 2, 3, 4, 5, + 6, 7, 8, 9, 10, 11, 12, + 13, 14, 15, 16, 17, 18]; + return allLevels.includes(x); +} + +function isValidAction(action?: LiftAction): boolean { + if(!action){ + return false; + } + if(action.type === "lift-move" || action.type === "door-open" ){ + return isLevel(action.level); + } + if(action.type === "add-destination" ){ + return isLevel(action.destination); + } + return false; +} diff --git a/src/shared.ts b/src/shared.ts new file mode 100644 index 0000000..77f7493 --- /dev/null +++ b/src/shared.ts @@ -0,0 +1,24 @@ +export type Lift = { + id: number, + level: number, + direction: string, + destinations: number[] +} + +export type SmallLiftRepresentation = { + id: number, + level: number, +} + +export type LiftFilterCriteron = { + floor?: number, + min_floor?: number, + max_floor?: number, + direction?: string, +} + + +export type LiftRequest = { + direction: string, + level: number, +} \ No newline at end of file