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
5 changes: 5 additions & 0 deletions .changeset/nice-shoes-mix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"xult": patch
---

fix: preserve `this` context for `func` functions
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,28 @@ if (validationErrorResult.isErr('FUNC_VALIDATION_ERROR')) {

<br>

### Class Methods

`Result.func` preserves the `this` context, allowing you to wrap methods that access instance properties.

```typescript
import { func, ok } from 'xult'

class User {
constructor(private name: string) {}

// Use a regular function to access `this`
greet = func(function(this: User) {
return ok(`Hello, I am ${this.name}`)
})
}

const user = new User('Alice')
const result = user.greet() // Ok('Hello, I am Alice')
```

<br>

### Elegant Workflows with Generators

This is where `xult` shines. Generator functions let you write sequential, business-friendly logic without the `if (result.isErr())` pyramid.
Expand Down
20 changes: 12 additions & 8 deletions src/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -598,17 +598,17 @@ export class Result<TValue, TError extends Result.LooseErrorShape> {

// Core logic for executing the function and processing its output.
// This is also async to handle all cases (promises, async generators).
const execute = (fnArgs: any[], out?: ReturnType<typeof fn>, isInitialCall = true): Result.Any | Promise<Result.Any> => {
const execute = (fnArgs: any[], out?: ReturnType<typeof fn>, isInitialCall = true, thisContext?: any): Result.Any | Promise<Result.Any> => {
try {
// only set out if missing AND this is the initial call
if (isInitialCall) {
out = fn(...fnArgs)
out = fn.call(thisContext, ...fnArgs)
}

// Await promises that are not generators
if (out instanceof Promise && typeof (<{ next?: unknown }>out).next !== 'function') {
return out
.then(o => execute(fnArgs, o, false))
.then(o => execute(fnArgs, o, false, thisContext))
.catch(toThrownError)
}

Expand Down Expand Up @@ -649,13 +649,17 @@ export class Result<TValue, TError extends Result.LooseErrorShape> {
}

if(schemas && schemas.length > 0) {
return (...args: any[]) => Result.validate(schemas, args).then(res => {
if(res.isErr()) return res
return execute(res.value!)
})
return function(this: any, ...args: any[]) {
return Result.validate(schemas, args).then(res => {
if(res.isErr()) return res
return execute(res.value!, undefined, true, this)
})
}
}

return (...args) => execute(args)
return function(this: any, ...args: any[]) {
return execute(args, undefined, true, this)
}
}

/** Throws an error if `this.isErr` otherwise, it returns the `result.value` */
Expand Down
58 changes: 58 additions & 0 deletions tests/func.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,64 @@ describe('Result.func', () => {
expect(called).toBe(false)
})
})

describe('Class Methods with func', () => {
class Example {
baseValue = 10

multiply = func((x: number, y: number) => x * y)

squared = func(function(this: Example, num: number) {
return `Example says: ${this.multiply(num, num).value}`
})

asyncAdd = func(async function(this: Example, n: number) {
return this.baseValue + n
})

genAdd = func(function*(this: Example, n: number) {
yield
return this.baseValue + n
})

asyncGenAdd = func(async function*(this: Example, n: number) {
yield
return this.baseValue + n
})
}

test('should wrap class methods and preserve this context (sync)', async () => {
const ex = new Example()
const r1 = ex.multiply(3, 4)
expect(r1.isOk()).toBe(true)
expect(r1.value).toBe(12)

const r2 = ex.squared(5)
expect(r2.isOk()).toBe(true)
expect(r2.value).toBe('Example says: 25')
})

test('should preserve this context in async functions', async () => {
const ex = new Example()
const r = await ex.asyncAdd(5)
expect(r.isOk()).toBe(true)
expect(r.value).toBe(15)
})

test('should preserve this context in generator functions', () => {
const ex = new Example()
const r = ex.genAdd(5)
expect(r.isOk()).toBe(true)
expect(r.value).toBe(15)
})

test('should preserve this context in async generator functions', async () => {
const ex = new Example()
const r = await ex.asyncGenAdd(5)
expect(r.isOk()).toBe(true)
expect(r.value).toBe(15)
})
})
})

describe('[type] Result.func', () => {
Expand Down