diff --git a/cli/package.json b/cli/package.json index 9804484..cb7b7b1 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,4 +1,14 @@ { "name": "cli", - "private": true + "private": true, + "devDependencies": { + "@types/node": "^18.11.11" + }, + "dependencies": { + "@effect/cli": "^0.12.0", + "@effect/platform": "^0.16.1", + "@effect/platform-node": "^0.17.0", + "@effect/schema": "^0.36.5", + "effect": "2.0.0-next.34" + } } diff --git a/cli/src/index.ts b/cli/src/index.ts new file mode 100644 index 0000000..2124b7b --- /dev/null +++ b/cli/src/index.ts @@ -0,0 +1,38 @@ +import { Effect, Option, Cause, Layer } from 'effect' +import * as Http from '@effect/platform-node/HttpClient' +import * as Node from '@effect/platform-node/Runtime' +import * as Command from '@effect/cli/Command' +import * as Cli from '@effect/cli/CliApp' +import * as Options from '@effect/cli/Options' +import * as Args from '@effect/cli/Args' +import * as Console from '@effect/cli/Console' +import * as Wttr from './wttr' + +const cli = Cli.make({ + name: 'Weather', + version: '1.2.3', + command: Command.make('weather', { + args: Args.between(Args.text({ name: 'location' }), 0, 1), + options: Options.all({ + url: Options.withDefault(Options.text('url'), 'https://wttr.in'), + }), + }) +}) + +Node.runMain(Effect.sync(() => process.argv.slice(2)).pipe( + Effect.flatMap((args) => + Cli.run(cli, args, ({ options, args }) => { + const location = Option.fromIterable(args) + const program = Wttr.Wttr.pipe( + Effect.flatMap(wttr => wttr.getWeather(location)), + // TODO: Render the output properly. + Effect.tap(Effect.log), + ) + + const wttr = Layer.provide(Http.client.layer, Wttr.makeLayer(options.url)) + return Effect.provideLayer(program, wttr) + }) + ), + Effect.provideLayer(Console.layer), + Effect.tapErrorCause((_) => Effect.logError(Cause.pretty(_))) +)) diff --git a/cli/src/wttr.ts b/cli/src/wttr.ts new file mode 100644 index 0000000..bb822d4 --- /dev/null +++ b/cli/src/wttr.ts @@ -0,0 +1,51 @@ +import { Effect, Data, Context, Layer, Option } from 'effect' +import * as Http from '@effect/platform/HttpClient' +import * as Schema from '@effect/schema/Schema' + +type WttrWeather = Schema.Schema.To +const WttrWeatherSchema = Schema.struct({ + date: Schema.dateFromString(Schema.string), + avgtempC: Schema.numberFromString(Schema.string), + avgtempF: Schema.numberFromString(Schema.string), + maxtempC: Schema.numberFromString(Schema.string), + maxtempF: Schema.numberFromString(Schema.string), + mintempC: Schema.numberFromString(Schema.string), + mintempF: Schema.numberFromString(Schema.string), + sunHour: Schema.numberFromString(Schema.string), + uvIndex: Schema.numberFromString(Schema.string) +}) + +const WttrResponseSchema = Schema.struct({ + weather: Schema.array(WttrWeatherSchema) +}) + +class WttrError extends Data.TaggedClass('WttrError')<{ + cause: unknown +}> {} + +export const Wttr = Context.Tag() +export interface Wttr { + readonly getWeather: (location: Option.Option) => Effect.Effect> +} + +export const makeLayer = (url: string) => Layer.effect(Wttr, Effect.gen(function* ($) { + const defaultClient = yield* $(Http.client.Client) + const wttrClient = defaultClient.pipe( + Http.client.mapRequest(Http.request.prependUrl(url)), + Http.client.mapRequest(Http.request.appendUrlParam('format', 'j1')), + Http.client.filterStatusOk, + Http.client.mapEffect(Http.response.schemaBodyJson(WttrResponseSchema)), + Http.client.catchTags({ + 'RequestError': (cause) => Effect.fail(new WttrError({ cause })), + 'ResponseError': (cause) => Effect.fail(new WttrError({ cause })), + 'ParseError': (cause) => Effect.fail(new WttrError({ cause })), + }), + ) + + return Wttr.of({ + getWeather: (location) => Option.match(location, { + onNone: () => Http.request.get('/'), + onSome: (_) => Http.request.get(`/${encodeURIComponent(_)}`), + }).pipe(wttrClient, Effect.map(_ => _.weather)), + }) +})) diff --git a/cli/tsconfig.json b/cli/tsconfig.json new file mode 100644 index 0000000..867e1e9 --- /dev/null +++ b/cli/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "noEmit": true, + "module": "Node16", + "moduleResolution": "Node16", + "strict": true, + "plugins": [{ "name": "@effect/language-service" }], + }, + "include": ["src/**/*.ts"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e34de36..4011490 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,5 +1,9 @@ lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + importers: .: @@ -8,7 +12,27 @@ importers: specifier: ^5.2.2 version: 5.2.2 - cli: {} + cli: + dependencies: + '@effect/cli': + specifier: ^0.12.0 + version: 0.12.0(@effect/data@0.18.6)(@effect/io@0.40.3)(@effect/printer-ansi@0.15.0)(@effect/printer@0.15.0) + '@effect/platform': + specifier: ^0.16.1 + version: 0.16.1(@effect/data@0.18.6)(@effect/io@0.40.3)(@effect/schema@0.36.5)(@effect/stream@0.36.1) + '@effect/platform-node': + specifier: ^0.17.0 + version: 0.17.0(@effect/data@0.18.6)(@effect/io@0.40.3)(@effect/schema@0.36.5)(@effect/stream@0.36.1) + '@effect/schema': + specifier: ^0.36.5 + version: 0.36.5(@effect/data@0.18.6)(@effect/io@0.40.3) + effect: + specifier: 2.0.0-next.34 + version: 2.0.0-next.34(@effect/data@0.18.6)(@effect/io@0.40.3)(@effect/match@0.35.2)(@effect/stm@0.24.0)(@effect/stream@0.36.1) + devDependencies: + '@types/node': + specifier: ^18.11.11 + version: 18.16.19 node-scripts: dependencies: @@ -1671,6 +1695,20 @@ packages: '@jridgewell/trace-mapping': 0.3.9 dev: true + /@effect/cli@0.12.0(@effect/data@0.18.6)(@effect/io@0.40.3)(@effect/printer-ansi@0.15.0)(@effect/printer@0.15.0): + resolution: {integrity: sha512-aNScD2gCqwuBcvjeGqraYgcY607n/rk4MmHA+SZjwey46VomRqzt6T3EZr7MMAM7/FRRLgxgsoAYVgwG5LWSKA==} + peerDependencies: + '@effect/data': ^0.18.3 + '@effect/io': ^0.40.0 + '@effect/printer': ^0.15.0 + '@effect/printer-ansi': ^0.15.0 + dependencies: + '@effect/data': 0.18.6 + '@effect/io': 0.40.3(@effect/data@0.18.6) + '@effect/printer': 0.15.0(@effect/data@0.18.6)(@effect/io@0.40.3)(@effect/typeclass@0.5.0) + '@effect/printer-ansi': 0.15.0(@effect/data@0.18.6)(@effect/io@0.40.3)(@effect/printer@0.15.0)(@effect/typeclass@0.5.0) + dev: false + /@effect/data@0.18.6: resolution: {integrity: sha512-gfjorojxEJ0KKxwluZLoAsPv1kkHi7MV1vQDlibClfdtC9iY9JqkJo+BjWTKsU0zV4mW2inmTZQCF72KKbn2tQ==} @@ -1735,6 +1773,32 @@ packages: path-browserify: 1.0.1 dev: false + /@effect/printer-ansi@0.15.0(@effect/data@0.18.6)(@effect/io@0.40.3)(@effect/printer@0.15.0)(@effect/typeclass@0.5.0): + resolution: {integrity: sha512-HvbjiL3htbFmb8VfQKNlNGQwdEqcb+DRETevoDXCxxBjMJcybrjGWVhOFZ/qixfVc+012iqvuZtsSSqxcLqpKQ==} + peerDependencies: + '@effect/data': ^0.18.3 + '@effect/io': ^0.40.0 + '@effect/printer': ^0.15.0 + '@effect/typeclass': ^0.5.0 + dependencies: + '@effect/data': 0.18.6 + '@effect/io': 0.40.3(@effect/data@0.18.6) + '@effect/printer': 0.15.0(@effect/data@0.18.6)(@effect/io@0.40.3)(@effect/typeclass@0.5.0) + '@effect/typeclass': 0.5.0(@effect/data@0.18.6) + dev: false + + /@effect/printer@0.15.0(@effect/data@0.18.6)(@effect/io@0.40.3)(@effect/typeclass@0.5.0): + resolution: {integrity: sha512-5IbgrBl7jULKxDP0nnWsFJJAqDwrqiCpVPLliN+Cxkn8a0XtrP0dOWBThLU98zkggGYkpv0A3We4or3OQx1Rjg==} + peerDependencies: + '@effect/data': ^0.18.3 + '@effect/io': ^0.40.0 + '@effect/typeclass': ^0.5.0 + dependencies: + '@effect/data': 0.18.6 + '@effect/io': 0.40.3(@effect/data@0.18.6) + '@effect/typeclass': 0.5.0(@effect/data@0.18.6) + dev: false + /@effect/schema@0.36.5(@effect/data@0.18.6)(@effect/io@0.40.3): resolution: {integrity: sha512-E8KZ17DqZJl7E/eEVTkxb2NEv6vYhab79HugHi10krd2Gm3vOJvWs+nRgtPhI+0dvkXVxzCcY8bLfkND2F0rBg==} peerDependencies: @@ -1764,6 +1828,14 @@ packages: '@effect/data': 0.18.6 '@effect/io': 0.40.3(@effect/data@0.18.6) + /@effect/typeclass@0.5.0(@effect/data@0.18.6): + resolution: {integrity: sha512-bhZg69c6yZcZChFtyTAsyn6Bzmbw4s1Vq/LBU8kVTtwFXz5J2kZSkI+e8LWW1xvtBOeGpNcPqPwfbItVYXe23A==} + peerDependencies: + '@effect/data': ^0.18.3 + dependencies: + '@effect/data': 0.18.6 + dev: false + /@effect/vite-plugin-react@0.0.6(@effect/data@0.18.6)(@effect/io@0.40.3)(@effect/match@0.35.2)(@effect/stm@0.24.0)(@effect/stream@0.36.1)(@vitejs/plugin-react@3.1.0)(babel-plugin-annotate-pure-calls@0.4.0)(typescript@5.1.6)(vite@4.4.9): resolution: {integrity: sha512-ed3iBFtfsDF/PmFnoeQKCylddr2N9sl7ZyTXG/++tEIf0XrY5+Usjgl0Cck00mkH08H/ECiaayg3viJPquLsQw==} peerDependencies: @@ -3176,6 +3248,7 @@ packages: /bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + requiresBuild: true dependencies: file-uri-to-path: 1.0.0 dev: true @@ -5165,6 +5238,7 @@ packages: /file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + requiresBuild: true dev: true optional: true @@ -7138,6 +7212,7 @@ packages: /node-addon-api@1.7.2: resolution: {integrity: sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==} + requiresBuild: true dev: true optional: true @@ -9606,7 +9681,3 @@ packages: /zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} dev: true - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false