Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added initial @untypeable/randomuser package #6

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions packages/randomuser/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# 🧘 @untypeable/randomuser

Untypeable router type definitions & validators for the random user generator API

## 🚀 Install

Install it locally in your project

```bash
# npm
npm install @untypeable/randomuser

# yarn
yarn add @untypeable/randomuser

# pnpm
pnpm install @untypeable/randomuser
```

## 🦄 Usage

Create a new client instance with the `LilRouter` & your desired fetch handler

```typescript
import { createTypeLevelClient } from "untypeable";

import type { RandomUserRouter } from "@untypeable/randomuser";

const client = createTypeLevelClient<RandomUserRouter>(async (path) => {
const url = new URL(path, "https://randomuser.me/");
Object.entries(input).forEach(([key, value]) =>
url.searchParams.append(key, value as string)
);

const response = await fetch(url.href);

return await response.json();
});

const randomUser = await client("/api");

const femaleUser = await client("/api", { gender: "female" });
```
76 changes: 76 additions & 0 deletions packages/randomuser/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
{
"name": "@untypeable/randomuser",
"version": "1.0.2",
"description": "Untypeable router type definitions & validators for the random user generator API",
"publishConfig": {
"access": "public"
},
"repository": {
"directory": "packages/randomuser",
"type": "git",
"url": "https://github.com/nurodev/untypeable.git"
},
"homepage": "https://lil.software/api",
"bugs": "https://github.com/nurodev/untypeable/issues",
"readme": "README.md",
"author": {
"name": "nurodev",
"email": "[email protected]",
"url": "https://nuro.dev"
},
"keywords": [
"api",
"random",
"randomuser",
"typescript",
"untypeable",
"user"
],
"license": "MIT",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": "./dist/index",
"./zod": "./dist/zod",
"./package.json": "./package.json"
},
"typesVersions": {
"*": {
"zod": [
"./dist/zod.d.ts"
]
}
},
"files": [
"dist/**/*",
"LICENSE",
"README.md"
],
"scripts": {
"build": "tsup",
"test": "vitest run",
"test:watch": "vitest watch",
"test:ui": "vitest watch --ui"
},
"dependencies": {
"untypeable": "^0.2.1"
},
"devDependencies": {
"@types/node": "^18.15.3",
"@vitest/ui": "^0.29.7",
"dotenv": "^16.0.3",
"typescript": "^4.9.5",
"undici": "^5.21.0",
"vitest": "^0.29.7",
"zod": "^3.21.4"
},
"peerDependencies": {
"zod": "^3.21.4"
},
"peerDependenciesMeta": {
"zod": {
"optional": true
}
}
}
22 changes: 22 additions & 0 deletions packages/randomuser/src/api/api.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { z } from "zod";

import type {
ApiSchema,
ApiInputSchema,
FieldSchema,
GenderSchema,
NationalitySchema,
UserSchema,
} from "./api.validators";

export type Api = z.infer<typeof ApiSchema>;

export type ApiInput = z.infer<typeof ApiInputSchema>;

export type Field = z.infer<typeof FieldSchema>;

export type Gender = z.infer<typeof GenderSchema>;

export type Nationality = z.infer<typeof NationalitySchema>;

export type User = z.infer<typeof UserSchema>;
125 changes: 125 additions & 0 deletions packages/randomuser/src/api/api.validators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { z } from "zod";

export const GenderSchema = z.enum(["male", "female"]);

export const FieldSchema = z.enum([
"cell",
"dob",
"email",
"gender",
"id",
"location",
"login",
"name",
"nat",
"phone",
"picture",
"registered",
]);

export const NationalitySchema = z.enum([
"AU",
"BR",
"CA",
"CH",
"DE",
"DK",
"ES",
"FI",
"FR",
"GB",
"IE",
"IN",
"IR",
"MX",
"NL",
"NO",
"NZ",
"RS",
"TR",
"UA",
"US",
]);

export const UserSchema = z.object({
cell: z.string(),
dob: z.object({
age: z.number(),
date: z.string().datetime(),
}),
email: z.string().email(),
gender: GenderSchema,
id: z.object({
name: z.string(),
value: z.string().nullable(),
}),
location: z.object({
city: z.string(),
coordinates: z.object({
latitude: z.string().transform(Number),
longitude: z.string().transform(Number),
}),
country: z.string(),
postcode: z.string().or(z.number()),
state: z.string(),
street: z.object({
name: z.string(),
number: z.number(),
}),
timezone: z.object({
description: z.string(),
offset: z.string(),
}),
}),
login: z.object({
md5: z.string(),
password: z.string(),
salt: z.string(),
sha1: z.string(),
sha256: z.string(),
username: z.string(),
uuid: z.string().uuid(),
}),
name: z.object({
first: z.string(),
last: z.string(),
title: z.string(),
}),
nat: NationalitySchema,
phone: z.string(),
picture: z.object({
large: z.string().url(),
medium: z.string().url(),
thumbnail: z.string().url(),
}),
registered: z.object({
age: z.number(),
date: z.string().datetime(),
}),
});

export const ApiSchema = z.object({
info: z.object({
page: z.number(),
results: z.number(),
seed: z.string(),
version: z.string(),
}),
results: z.array(UserSchema),
});

export const ApiInputSchema = z
.object({
exc: z.array(FieldSchema),
format: z.enum(["csv", "json", "prettyjson", "xml", "yaml"]),
gender: GenderSchema,
inc: z.array(FieldSchema),
nat: NationalitySchema,
noinfo: z.boolean(),
page: z.number(),
password: z.array(z.string()),
results: z.number(),
seed: z.string(),
version: z.enum(["1.0", "1.1", "1.2", "1.3", "1.4"]),
})
.partial();
13 changes: 13 additions & 0 deletions packages/randomuser/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { initUntypeable } from "untypeable";

import type { Api, ApiInput } from "./api/api.types";

const u = initUntypeable();

const router = u.router({
"/api": u.input<ApiInput>().output<Api>(),
});

export type RandomUserRouter = typeof router;

export * from "./api/api.types";
1 change: 1 addition & 0 deletions packages/randomuser/src/zod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./api/api.validators";
28 changes: 28 additions & 0 deletions packages/randomuser/tests/_client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { beforeAll } from "vitest";
import { createTypeLevelClient } from "untypeable";
import { fetch } from "undici";

import type { RandomUserRouter } from "../src";

export function useTestClient() {
const client = createTypeLevelClient<RandomUserRouter>(
async (path, input = {}) => {
const url = new URL(path, "https://randomuser.me/");
Object.entries(input).forEach(([key, value]) =>
url.searchParams.append(key, value as string)
);

const response = await fetch(url.href);
if (!response.ok)
throw new Error(`HTTP ${response.status} ${response.statusText}`);

return await response.json();
}
);

beforeAll(() => {
if (!client) throw "Failed to initialise untypeable client instance.";
});

return client;
}
40 changes: 40 additions & 0 deletions packages/randomuser/tests/api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { describe, it, expect } from "vitest";

import { ApiSchema, UserSchema } from "../src/zod";
import { useTestClient } from "./_client";

describe.concurrent("Random User - API", () => {
const client = useTestClient();

it("GET - /api", async () => {
const randomUser = await client("/api");

expect(randomUser).toBeDefined();
expect(randomUser.results).toBeDefined();
expect(randomUser.results.length).toBeGreaterThan(0);
expect(randomUser.results.at(0)).toBeDefined();

expect(ApiSchema.safeParse(randomUser).success).toBe(true);
expect(UserSchema.safeParse(randomUser.results.at(0)).success).toBe(true);
});

it("GET - /api?seed=foobar&gender=female", async () => {
const gender = "female";
const seed = "foobar";

const randomUser = await client("/api", {
gender,
seed,
});

expect(randomUser).toBeDefined();
expect(randomUser.results).toBeDefined();
expect(randomUser.results.length).toBeGreaterThan(0);
expect(randomUser.results.at(0)).toBeDefined();
expect(randomUser.results.at(0)?.gender).toBe(gender);
expect(randomUser.info.seed).toBe(seed);

expect(ApiSchema.safeParse(randomUser).success).toBe(true);
expect(UserSchema.safeParse(randomUser.results.at(0)).success).toBe(true);
});
});
16 changes: 16 additions & 0 deletions packages/randomuser/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"esModuleInterop": true,
"lib": ["ESNext"],
"module": "ESNext",
"moduleResolution": "Node",
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"resolveJsonModule": true,
"sourceMap": true,
"strict": true,
"target": "ESNext"
},
"include": ["./src", "./tests/*.test.ts"]
}
15 changes: 15 additions & 0 deletions packages/randomuser/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { defineConfig } from "tsup";

export default defineConfig(({ watch = false }) => ({
clean: true,
dts: {
resolve: true,
},
entry: {
index: "src/index.ts",
zod: "src/zod.ts",
},
format: ["cjs", "esm"],
minify: true,
watch,
}));