adapté à la CLEAN

This commit is contained in:
Julien LEICHER 2022-03-24 09:57:45 +01:00
parent f540d03c1d
commit 31671d4ec0
No known key found for this signature in database
GPG Key ID: BE0761B6A007EB96
26 changed files with 7714 additions and 40 deletions

View File

@ -12,5 +12,7 @@ POST {{url}}/locations
Content-Type: application/json Content-Type: application/json
{ {
"name": "Bourg-Achard" "name": "Bourg-Achard 2",
"lat": 49.443232,
"lon": 1.099971
} }

6
jest.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
testEnvironment: "node",
transform: {
"^.+\\.tsx?$": "ts-jest",
},
};

7181
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@
"description": "", "description": "",
"scripts": { "scripts": {
"start": "ts-node src/index.ts", "start": "ts-node src/index.ts",
"test": "echo \"Error: no test specified\" && exit 1" "test": "jest"
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",
@ -13,7 +13,10 @@
"@types/node-fetch": "^2.6.1", "@types/node-fetch": "^2.6.1",
"@types/uuid": "^8.3.4", "@types/uuid": "^8.3.4",
"ts-node": "^10.5.0", "ts-node": "^10.5.0",
"typescript": "^4.5.5" "typescript": "^4.5.5",
"@types/jest": "27.4.1",
"jest": "27.5.1",
"ts-jest": "27.1.3"
}, },
"dependencies": { "dependencies": {
"better-sqlite3": "^7.5.0", "better-sqlite3": "^7.5.0",

67
solid/employee.ts Normal file
View File

@ -0,0 +1,67 @@
class Employee_ {
// Nom, prénom, etc...
public reportHours() {}
public calculatePay() {}
public save() {}
}
class Employee {
public constructor(private email: string) {}
// Nom, prénom, etc...
}
class PayCalculator {
calculatePay(employee: Employee) {}
}
class HoursReporter {
reportHours(employee: Employee) {}
}
class EmployeeSaver {
save(employee: Employee) {}
}
interface EmployeeRepository {
save(employee: Employee);
}
class PostgresRepository implements EmployeeRepository {
save(employee: Employee) {
throw new Error("Method not implemented.");
}
}
function registerEmployee(repository: EmployeeRepository, email: string) {
// Validation mot de passe, email, unicité
const empl = new Employee(email);
repository.save(empl);
}
// Dans mes tests unitaires
class InMemoryRepository implements EmployeeRepository {
constructor(public employees: Employee[] = []) {}
save(employee: Employee) {
this.employees.push(employee);
}
}
function TestRegisterUser() {
const repository = new InMemoryRepository();
registerEmployee(repository, "un@email.com");
// assert(repository.employees.length) == 1
}
function TestRegisterUserDuplicateEmail() {
const repository = new InMemoryRepository([new Employee("un@email.com")]);
registerEmployee(repository, "un@email.com");
// assert erreur levée car email dupliqué
}

View File

@ -28,7 +28,7 @@ class Square implements Shape {
} }
class AreaCalculator { class AreaCalculator {
constructor(private readonly shapes: Shape[]) {} constructor(protected readonly shapes: Shape[]) {}
public sum(): number { public sum(): number {
return this.shapes.reduce((total, shape) => total + shape.area(), 0); return this.shapes.reduce((total, shape) => total + shape.area(), 0);
@ -38,7 +38,7 @@ class AreaCalculator {
class VolumeCalculator extends AreaCalculator { class VolumeCalculator extends AreaCalculator {
public sum(): number { public sum(): number {
// logique de calcul, on retournerait la somme des volumes // logique de calcul, on retournerait la somme des volumes
return 0; return this.shapes.reduce((total, shape) => total + shape.volume(), 0);
} }
} }

View File

@ -1,5 +1,10 @@
interface ReaderAndWriter extends Writer, Reader {} interface ReaderAndWriter extends Writer, Reader {}
interface File {
write(data: any): void;
read(): any;
}
interface Writer { interface Writer {
write(data: any): void; write(data: any): void;
} }

View File

@ -0,0 +1,10 @@
import { FastifyReply } from "fastify";
import Presenter from "../usecases/presenter";
export default class FastifyPresenter<T> implements Presenter<T> {
constructor(private readonly res: FastifyReply) {}
show(result: T): void {
this.res.status(200).send(result);
}
}

View File

@ -0,0 +1,14 @@
import LocationsRepository from "../entities/locationsRepository";
import Location from "../entities/location";
export class InMemoryLocationRepository implements LocationsRepository {
constructor(public locations: Location[] = []) {}
getAll(): Location[] {
return this.locations;
}
save(location: Location): void {
this.locations.push(location);
}
}

View File

@ -0,0 +1,22 @@
import fetch from "node-fetch";
import WeatherService, { Forecast } from "../entities/weatherService";
export default class OpenWeatherService implements WeatherService {
constructor(
private readonly appid: string,
private readonly units: string = "metric"
) {}
async getForecastFor(lat: number, lon: number): Promise<Forecast> {
const { weather, main } = await fetch(
`https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=${this.appid}&units=${this.units}`
).then((r) => r.json());
return {
temperature: main.temp,
min: main.temp_min,
max: main.temp_max,
weather: weather[0].main,
};
}
}

View File

@ -0,0 +1,29 @@
import { Database } from "better-sqlite3";
import Location from "../entities/location";
import LocationsRepository from "../entities/locationsRepository";
export default class SqliteLocationsRepository implements LocationsRepository {
constructor(private readonly db: Database) {}
getAll(): Location[] {
return this.db
.prepare("SELECT id, name, lat, lon FROM locations")
.all()
.map((locationData) => {
return Object.setPrototypeOf(locationData, Location);
});
}
save(location: Location): void {
this.db
.prepare(
"INSERT INTO locations (id, name, lat, lon) VALUES (@id, @name, @lat, @lon)"
)
.run({
id: location.id,
name: location.name,
lat: location.lat,
lon: location.lon,
});
}
}

View File

@ -0,0 +1,16 @@
import Location from "./location";
describe("entity location", () => {
it("can be created", () => {
const name = "Bourg-Achard";
const lat = 49.443232;
const lon = 1.099971;
const location = new Location(name, lat, lon);
expect(location.id).not.toBeUndefined();
expect(location.name).toEqual(name);
expect(location.lat).toEqual(lat);
expect(location.lon).toEqual(lon);
});
});

View File

@ -0,0 +1,13 @@
import { v4 as uuid } from "uuid";
export default class Location {
public id: string;
public constructor(
public name: string,
public lat: number,
public lon: number
) {
this.id = uuid();
}
}

View File

@ -0,0 +1,6 @@
import Location from "./location";
export default interface LocationsRepository {
getAll(): Location[];
save(location: Location): void; // On pourrait séparer lecture / écriture
}

View File

@ -0,0 +1,10 @@
export type Forecast = {
weather: string;
temperature: number;
min: number;
max: number;
};
export default interface WeatherService {
getForecastFor(lat: number, lon: number): Promise<Forecast>;
}

View File

@ -0,0 +1,33 @@
import { InMemoryLocationRepository } from "../adapters/inMemoryLocationsRepository";
import Location from "../entities/location";
import createLocation, { CreateLocationData } from "./createLocation";
import Presenter from "./presenter";
export class DummyPresenter<T> implements Presenter<T> {
constructor(public result?: T) {}
show(result: T): void {
this.result = result;
}
}
describe("use case createLocation", () => {
it("persist a location", () => {
const cmd: CreateLocationData = {
name: "Bourg-Achard",
lat: 49.443232,
lon: 1.099971,
};
const presenter = new DummyPresenter<Location>();
const repository = new InMemoryLocationRepository();
createLocation(repository, presenter, cmd);
expect(repository.locations).toHaveLength(1);
const newLocation = repository.locations[0];
expect(newLocation.name).toEqual(cmd.name);
expect(newLocation.lat).toEqual(cmd.lat);
expect(newLocation.lon).toEqual(cmd.lon);
});
});

View File

@ -0,0 +1,72 @@
import Location from "../entities/location";
import LocationsRepository from "../entities/locationsRepository";
import Presenter from "./presenter";
// On pourrait l'injecter dans le use case
// export interface CreateLocationDataValidator {
// validate(cmd: CreateLocationData): void;
// }
export type CreateLocationData = {
name: string;
lat: number;
lon: number;
};
// interface UseCase<TCommand> {
// execute(cmd: TCommand): void;
// }
// class CreateLocationUseCase implements UseCase<CreateLocationData> {
// public constructor(private readonly locationRepository: LocationRepository) {}
// execute(cmd: CreateLocationData): void {
// // FIXME: Valider les entrées utilisateurs
// if (typeof cmd !== "object") {
// throw new Error("should be an object");
// }
// const location = new Location(cmd.name, cmd.lat, cmd.lon);
// this.locationRepository.save(location);
// }
// }
// const db = new Database("monfichier.db")
// const sqliteRepository = new SqliteLocationRepository(db);
// const uc = new CreateLocationUseCase(sqliteRepository)
// uc.execute({
// })
// createLocation(sqliteRepository, {
// });
export default function createLocation(
locationRepository: LocationsRepository,
presenter: Presenter<Location>,
cmd: CreateLocationData
) {
// FIXME: Valider les entrées utilisateurs
if (typeof cmd !== "object") {
throw new Error("should be an object");
}
const location = new Location(cmd.name, cmd.lat, cmd.lon);
locationRepository.save(location);
presenter.show(location);
}
// export function createLocation_(
// locationRepository: LocationsRepository,
// cmd: CreateLocationData
// ): Location {
// const location = new Location(cmd.name, cmd.lat, cmd.lon);
// locationRepository.save(location);
// return location;
// }

View File

@ -0,0 +1,48 @@
import { InMemoryLocationRepository } from "../adapters/inMemoryLocationsRepository";
import Location from "../entities/location";
import WeatherService, { Forecast } from "../entities/weatherService";
import { DummyPresenter } from "./createLocation.test";
import getForecasts, { GetForecastsResult } from "./getForecasts";
class DummyWeatherService implements WeatherService {
getForecastFor(lat: number, lon: number): Promise<Forecast> {
return Promise.resolve({
weather: "Sunny",
temperature: 12,
min: 10,
max: 16,
});
}
}
describe("use case getForecasts", () => {
it("retrieve all locations forecasts", async () => {
const repository = new InMemoryLocationRepository([
new Location("Rouen", 49.443232, 1.099971),
new Location("Bourg-Achard", 49.35322, 0.81623),
]);
const presenter = new DummyPresenter<GetForecastsResult[]>();
const weatherService = new DummyWeatherService();
await getForecasts(presenter, repository, weatherService);
expect(presenter.result).not.toBeUndefined();
expect(presenter.result).toHaveLength(2);
let forecast = presenter.result![0];
expect(forecast.location.name).toEqual("Rouen");
expect(forecast.forecast.max).toEqual(16);
expect(forecast.forecast.min).toEqual(10);
expect(forecast.forecast.temperature).toEqual(12);
expect(forecast.forecast.weather).toEqual("Sunny");
forecast = presenter.result![1];
expect(forecast.location.name).toEqual("Bourg-Achard");
expect(forecast.forecast.max).toEqual(16);
expect(forecast.forecast.min).toEqual(10);
expect(forecast.forecast.temperature).toEqual(12);
expect(forecast.forecast.weather).toEqual("Sunny");
});
});

View File

@ -0,0 +1,32 @@
import Location from "../entities/location";
import LocationsRepository from "../entities/locationsRepository";
import WeatherService, { Forecast } from "../entities/weatherService";
import Presenter from "./presenter";
export type GetForecastsResult = {
location: Location;
forecast: Forecast;
};
export default async function getForecasts(
presenter: Presenter<GetForecastsResult[]>,
repository: LocationsRepository,
weatherService: WeatherService
) {
const locations = repository.getAll();
const forecasts = await Promise.all(
locations.map(async (location) => {
const forecast = await weatherService.getForecastFor(
location.lat,
location.lon
);
return {
location,
forecast,
};
})
);
presenter.show(forecasts);
}

View File

@ -0,0 +1,24 @@
import LocationsRepository from "../entities/locationsRepository";
import WeatherService from "../entities/weatherService";
import { CreateLocationData } from "./createLocation";
export default class LocationService {
constructor(
private readonly repository: LocationsRepository,
private readonly weatherService: WeatherService
) {}
createLocation(data: CreateLocationData) {
// TODO
// const location = new Location()
// this.repository.save(location);
}
getLocations() {
// TODO
}
getForecasts() {
// TODO
}
}

View File

@ -0,0 +1,3 @@
export default interface Presenter<T> {
show(result: T): void;
}

View File

@ -0,0 +1,19 @@
import { InMemoryLocationRepository } from "../adapters/inMemoryLocationsRepository";
import Location from "../entities/location";
import { DummyPresenter } from "./createLocation.test";
import showLocations from "./showLocations";
describe("use case showsLocations", () => {
it("show every locations", () => {
const repository = new InMemoryLocationRepository([
new Location("Rouen", 49.443232, 1.099971),
new Location("Bourg-Achard", 49.35322, 0.81623),
]);
const presenter = new DummyPresenter<Location[]>();
showLocations(repository, presenter);
expect(presenter.result).not.toBeUndefined();
expect(presenter.result).toHaveLength(2);
});
});

View File

@ -0,0 +1,11 @@
import Location from "../entities/location";
import LocationsRepository from "../entities/locationsRepository";
import Presenter from "./presenter";
export default function showLocations(
repository: LocationsRepository,
presenter: Presenter<Location[]>
) {
const locations = repository.getAll();
presenter.show(locations);
}

54
src/index.old.ts Normal file
View File

@ -0,0 +1,54 @@
import fastify from "fastify";
import Database from "better-sqlite3";
import fetch from "node-fetch";
import { v4 as uuid } from "uuid";
const db = new Database("weathery.db");
const APPID = "d6f0f985f37372e9824c68a52662cc23";
const app = fastify({
logger: true,
});
app.post("/locations", async (req) => {
const payload: any = req.body;
const location = {
id: uuid(),
name: payload.name,
};
db.prepare("INSERT INTO locations (id, name) VALUES (@id, @name)").run(
location
);
return location;
});
app.get("/locations", async () => {
const locations = db.prepare("SELECT * FROM locations").all();
return { locations };
});
app.get("/", async () => {
const locations = db.prepare("SELECT * FROM locations").all();
const weathers = await Promise.all(
locations.map(async (location) => {
const { lat, lon } = location;
const { weather, main: properties } = await fetch(
`https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=${APPID}&units=metric`
).then((r) => r.json());
return {
...location,
weather,
properties,
};
})
);
return weathers;
});
app.listen(8080);

View File

@ -1,7 +1,13 @@
import fastify from "fastify"; import fastify from "fastify";
import showLocations from "./clean/usecases/showLocations";
import LocationsRepository from "./clean/entities/locationsRepository";
import FastifyPresenter from "./clean/adapters/fastifyPresenter";
import SqliteLocationsRepository from "./clean/adapters/sqliteLocationsRepository";
import Database from "better-sqlite3"; import Database from "better-sqlite3";
import fetch from "node-fetch"; import createLocation from "./clean/usecases/createLocation";
import { v4 as uuid } from "uuid"; import getForecasts from "./clean/usecases/getForecasts";
import OpenWeatherService from "./clean/adapters/openWeatherService";
import WeatherService from "./clean/entities/weatherService";
const db = new Database("weathery.db"); const db = new Database("weathery.db");
const APPID = "d6f0f985f37372e9824c68a52662cc23"; const APPID = "d6f0f985f37372e9824c68a52662cc23";
@ -10,45 +16,33 @@ const app = fastify({
logger: true, logger: true,
}); });
app.post("/locations", async (req) => { // const locationsRepository = new InMemoryLocationRepository([
const payload: any = req.body; // new Location("Rouen", 49.443232, 1.099971),
const location = { // new Location("Bourg-Achard", 49.35322, 0.81623),
id: uuid(), // ]);
name: payload.name, const locationsRepository: LocationsRepository = new SqliteLocationsRepository(
}; db
);
const weatherService: WeatherService = new OpenWeatherService(APPID);
db.prepare("INSERT INTO locations (id, name) VALUES (@id, @name)").run( app.get("/locations", async (_, res) => {
location showLocations(locationsRepository, new FastifyPresenter(res));
);
return location;
}); });
app.get("/locations", async () => { app.get("/", async (_, res) => {
const locations = db.prepare("SELECT * FROM locations").all(); await getForecasts(
new FastifyPresenter(res),
return { locations }; locationsRepository,
weatherService
);
}); });
app.get("/", async () => { app.post("/locations", async (req, res) => {
const locations = db.prepare("SELECT * FROM locations").all(); createLocation(
locationsRepository,
const weathers = await Promise.all( new FastifyPresenter(res),
locations.map(async (location) => { req.body as any
const { lat, lon } = location;
const { weather, main: properties } = await fetch(
`https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=${APPID}&units=metric`
).then((r) => r.json());
return {
...location,
weather,
properties,
};
})
); );
return weathers;
}); });
app.listen(8080); app.listen(8080);

Binary file not shown.