Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,4 @@ jobs:
NPM_TOKEN: ${{ secrets.NPM_API_TOKEN }}

- name: Publish to JSR
run: npx jsr publish
run: npx jsr publish --allow-dirty
11 changes: 11 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ This file contains the documentation on the notable changes and bug fixes, and
is formatted following this [standard](https://keepachangelog.com/en/1.0.0/).
This project also adheres to [Semantic Versioning](https://semver.org/).

## [2.1.0] - 2025-12-30

**Added**:

- Made the name set iterable (i.e., for-of statements)
- Added ability for deep equal between 2 namefully instances
- Made certain types flexible so callers aren’t boxed into enums
- Added support for JSON-serializable names
- Extended ALLOWED_FORMAT_TOKENS with more symbols
- Allowed dirty TS (e.g., example/*) when publish JSR

## [2.0.2] - 2025-12-02

**Fixed**:
Expand Down
2 changes: 1 addition & 1 deletion example/advanced.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ function main() {
builder.add(Name.prefix('Mr'));

// Build the name with options if needed
name = builder.build({ title: Title.US })
name = builder.build({ title: Title.US });
console.log(name.full); // Mr. Nikola Tesla
}

Expand Down
2 changes: 1 addition & 1 deletion jsr.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ralflorent/namefully",
"version": "2.0.2",
"version": "2.1.0",
"description": "Handle personal names in a particular order, way, or shape.",
"keywords": ["format", "parse", "human", "personal", "family", "name"],
"exports": "./src/index.ts",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "namefully",
"version": "2.0.2",
"version": "2.1.0",
"description": "Handle personal names in a particular order, way, or shape.",
"author": "Ralph Florent",
"license": "MIT",
Expand Down
6 changes: 3 additions & 3 deletions src/builder.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Name } from './name.js';
import { Config } from './config.js';
import { Namefully } from './namefully.js';
import { ArrayNameValidator } from './validator.js';
import { Namefully, NameOptions } from './namefully.js';

type VoidCallback = () => void;
type Callback<Type, Return> = (value: Type) => Return;
Expand Down Expand Up @@ -141,13 +141,13 @@ export class NameBuilder extends Builder<Name, Namefully> {
* Regardless of how the names are added, both first and last names must exist
* to complete a fine build. Otherwise, it throws a NameError.
*/
build(config?: Partial<Config>): Namefully {
build(options?: NameOptions): Namefully {
this.prebuild?.();

const names = [...this.queue];
ArrayNameValidator.create().validate(names);

this.instance = new Namefully(names, config);
this.instance = new Namefully(names, options);
this.postbuild?.(this.instance);

return this.instance;
Expand Down
27 changes: 2 additions & 25 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,4 @@
export const VERSION = '2.0.2';
export const VERSION = '2.1.0';
export const MIN_NUMBER_OF_NAME_PARTS = 2;
export const MAX_NUMBER_OF_NAME_PARTS = 5;
export const ALLOWED_FORMAT_TOKENS = [
'.',
',',
' ',
'-',
'_',
'b',
'B',
'f',
'F',
'l',
'L',
'm',
'M',
'n',
'N',
'o',
'O',
'p',
'P',
's',
'S',
'$',
];
export const ALLOWED_FORMAT_TOKENS = ` .,_-()[]<>'"bBfFlLmMnNoOpPsS$`;
258 changes: 258 additions & 0 deletions src/data.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
import { Namefully } from './namefully.js';
import { InputError, UnknownError } from './error.js';
import { Name, FirstName, LastName } from './name.js';
import { deserialize, SerializedName } from './data.js';

describe('JSON serialization', () => {
describe('serialize', () => {
test('should serialize a simple name', () => {
const name = new Namefully('John Smith');
const serialized = name.serialize();

expect(serialized).toEqual({
names: { firstName: 'John', lastName: 'Smith' },
config: expect.objectContaining({
name: expect.any(String),
orderedBy: expect.any(String),
separator: expect.any(String),
title: expect.any(String),
ending: expect.any(Boolean),
bypass: expect.any(Boolean),
surname: expect.any(String),
}),
});
});

test('should serialize a name with prefix and suffix', () => {
const name = new Namefully([Name.prefix('Mr'), Name.first('John'), Name.last('Smith'), Name.suffix('Jr')]);
const serialized = name.serialize();

expect(serialized.names.prefix).toBe('Mr');
expect(serialized.names.firstName).toBe('John');
expect(serialized.names.lastName).toBe('Smith');
expect(serialized.names.suffix).toBe('Jr');
});

test('should serialize a name with middle names', () => {
const name = new Namefully([
Name.first('John'),
Name.middle('Michael'),
Name.middle('David'),
Name.last('Smith'),
]);
const serialized = name.serialize();

expect(serialized.names.firstName).toBe('John');
expect(serialized.names.middleName).toEqual(['Michael', 'David']);
expect(serialized.names.lastName).toBe('Smith');
});

test('should serialize a name with multiple first names', () => {
const name = new Namefully([new FirstName('John', 'Michael'), Name.last('Smith')]);
const serialized = name.serialize();

expect(serialized.names.firstName).toEqual({ value: 'John', more: ['Michael'] });
expect(serialized.names.lastName).toBe('Smith');
});

test('should serialize a name with hyphenated last name', () => {
const name = new Namefully([Name.first('John'), new LastName('Smith', 'Jones')]);
const serialized = name.serialize();

expect(serialized.names.firstName).toBe('John');
expect(serialized.names.lastName).toEqual({ father: 'Smith', mother: 'Jones' });
});

test('should serialize a complex name with all parts', () => {
const name = new Namefully([
Name.prefix('Dr'),
new FirstName('John', 'Michael'),
Name.middle('David'),
new LastName('Smith', 'Jones'),
Name.suffix('PhD'),
]);
const serialized = name.serialize();

expect(serialized.names.prefix).toBe('Dr');
expect(serialized.names.firstName).toEqual({ value: 'John', more: ['Michael'] });
expect(serialized.names.middleName).toEqual(['David']);
expect(serialized.names.lastName).toEqual({ father: 'Smith', mother: 'Jones' });
expect(serialized.names.suffix).toBe('PhD');
});
});

describe('deserialize', () => {
test('should deserialize a simple name from object', () => {
const data: SerializedName = {
names: {
firstName: 'John',
lastName: 'Smith',
},
config: {
name: 'fullName',
orderedBy: 'firstName',
separator: ' ',
title: 'US',
ending: false,
bypass: false,
surname: 'father',
},
};

const name = deserialize(data);
expect(name.firstName()).toBe('John');
expect(name.lastName()).toBe('Smith');
expect(name.full).toBe('John Smith');
});

test('should deserialize a name from JSON string', () => {
const jsonString = JSON.stringify({
names: {
firstName: 'Jane',
lastName: 'Doe',
},
config: {
name: 'fullName',
orderedBy: 'firstName',
separator: ' ',
title: 'US',
ending: false,
bypass: false,
surname: 'father',
},
});

const name = deserialize(jsonString);
expect(name.firstName()).toBe('Jane');
expect(name.lastName()).toBe('Doe');
});

test('should deserialize a name with prefix and suffix', () => {
const original = new Namefully([Name.prefix('Mr'), Name.first('John'), Name.last('Smith'), Name.suffix('Jr')]);
const serialized = original.serialize();

const name = deserialize(serialized);
expect(name.prefix).toBe(original.prefix);
expect(name.suffix).toBe(original.suffix);
});

test('should deserialize a name with middle names', () => {
const data: SerializedName = {
names: {
firstName: 'John',
middleName: ['Michael', 'David'],
lastName: 'Smith',
},
config: {
name: 'fullName',
orderedBy: 'firstName',
separator: ' ',
title: 'US',
ending: false,
bypass: false,
surname: 'father',
},
};

const name = deserialize(data);
expect(name.middleName()).toEqual(['Michael', 'David']);
});

test('should deserialize a name with multiple first names', () => {
const data: SerializedName = {
names: {
firstName: {
value: 'John',
more: ['Michael'],
},
lastName: 'Smith',
},
config: {
name: 'fullName',
orderedBy: 'firstName',
separator: ' ',
title: 'US',
ending: false,
bypass: false,
surname: 'father',
},
};

const name = deserialize(data);
expect(name.firstName(false)).toBe('John');
});

test('should deserialize a name with hyphenated last name', () => {
const data: SerializedName = {
names: {
firstName: 'John',
lastName: {
father: 'Smith',
mother: 'Jones',
},
},
config: {
name: 'fullName',
orderedBy: 'firstName',
separator: ' ',
title: 'US',
ending: false,
bypass: false,
surname: 'father',
},
};

const name = deserialize(data);
expect(name.lastName()).toBe('Smith');
});

test('should throw NameError for invalid data', () => {
expect(() => deserialize(null as any)).toThrow(InputError);
expect(() => deserialize(undefined as any)).toThrow(InputError);
expect(() => deserialize(123 as any)).toThrow(InputError);
expect(() => deserialize('invalid json')).toThrow(UnknownError);
expect(() => deserialize('{ invalid }')).toThrow(UnknownError);
expect(() => deserialize({} as any)).toThrow(UnknownError);
});
});

describe('round-trip', () => {
test('should serialize and deserialize a simple name', () => {
const original = new Namefully('John Smith', { name: 'round-trip' });
const serialized = original.serialize();
const deserialized = deserialize(serialized);

expect(deserialized.config.name).toBe(original.config.name);
expect(deserialized.full).toBe(original.full);
expect(deserialized.firstName()).toBe(original.firstName());
expect(deserialized.lastName()).toBe(original.lastName());
});

test('should serialize and deserialize a complex name', () => {
const original = new Namefully([
Name.prefix('Dr'),
new FirstName('John', 'Michael'),
Name.middle('David'),
new LastName('Smith', 'Jones'),
Name.suffix('PhD'),
]);
const serialized = original.serialize();
const deserialized = deserialize(serialized);

expect(deserialized.prefix).toBe(original.prefix);
expect(deserialized.firstName()).toBe(original.firstName());
expect(deserialized.middleName()).toEqual(original.middleName());
expect(deserialized.lastName()).toBe(original.lastName());
expect(deserialized.suffix).toBe(original.suffix);
});

test('should serialize and deserialize from JSON string', () => {
const original = new Namefully([Name.first('Jane'), Name.last('Doe')]);
const serialized = original.serialize();
const jsonString = JSON.stringify(serialized);
const deserialized = deserialize(jsonString);

expect(deserialized.full).toBe(original.full);
});
});
});
Loading