Skip to content
Open
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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,15 @@ Console.WriteLine($"You picked {string.Join(", ", value)}");

### Bind (Model-based prompts)

Bind automatically generates a sequence of prompts from a model class. The model must be a `class` with a public parameterless constructor and accessible property setters, annotated with `[PromptBindable]`:

```csharp
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Sharprompt;

// Input model definition
[PromptBindable]
public class MyFormModel
{
[Display(Name = "What's your name?")]
Expand All @@ -137,8 +144,14 @@ public class MyFormModel
[InlineItems("Seattle", "London", "Tokyo")]
public string City { get; set; }

[Display(Name = "Select favorite fruit")]
[MemberItems(nameof(Fruits))]
public string Fruit { get; set; }

[Display(Name = "Are you ready?")]
public bool? Ready { get; set; }

public static IEnumerable<string> Fruits => new[] { "Apple", "Banana", "Orange" };
}

var result = Prompt.Bind<MyFormModel>();
Expand Down
76 changes: 76 additions & 0 deletions docs/en/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,79 @@ var city = Prompt.Select(new SelectOptions<string>
LoopingSelection = false
});
```

## Native AOT & Source Generators

Sharprompt is configured with `<IsAotCompatible>true</IsAotCompatible>` and supports Native AOT compilation on .NET 8 and .NET 10.

### How It Works

Sharprompt uses Roslyn Incremental Source Generators to generate code at compile time, eliminating runtime reflection:

1. **`[PromptBindable]` Generator**: Inspects classes annotated with `[PromptBindable]` and generates strongly typed model binding code registered via a C# `[ModuleInitializer]`. When invoking `Prompt.Bind<T>()`, Sharprompt uses the precompiled binder with zero reflection.
2. **Enum Metadata Generator**: Inspects enum declarations in your project and generates metadata (values, `[Display]` names, and ordering) registered via a `[ModuleInitializer]`.

### Project Configuration & Publishing

To publish an application with Native AOT, add `<PublishAot>true</PublishAot>` to your project file:

```xml
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<PublishAot>true</PublishAot>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Sharprompt" Version="..." />
</ItemGroup>

</Project>
```

Publish your application using the `dotnet publish` command targeting your runtime:

```sh
dotnet publish -c Release -r win-x64
```

Replace `win-x64` with your target runtime identifier (such as `linux-x64` or `osx-arm64`).

### Enum Handling & External Assemblies

- **Project Enums**: Any enum declared within the consuming project is automatically analyzed by the source generator at compile time. Display names and custom ordering specified with `[Display]` are generated directly into code.
- **External Enums**: Enums from external assemblies (such as BCL enums like `DayOfWeek` or enums from referenced packages) are not analyzed by the source generator. At runtime, Sharprompt falls back to an AOT-safe reflection mechanism. To avoid reflection entirely or customize the display, register the enum manually at startup:

```csharp
using Sharprompt;

EnumMetadataRegistry.Register<DayOfWeek>(
Enum.GetValues<DayOfWeek>(),
day => day.ToString()
);
```

### Type Conversion & Custom Types

Prompts rely on `TypeDescriptor` converters to parse user input. Built-in intrinsic converters (such as `string`, numeric types, `bool`, `char`, `Guid`, `DateTime`, and enums) are fully preserved under Native AOT.

If using custom types annotated with `[TypeConverter]`, ensure your application preserves the converter type from trimming (for example, using `[DynamicallyAccessedMembers]` or trimming configuration).

### Cancellation

All prompts, including model binding, support cancellation via `CancellationToken`. Pass the token to `Prompt.Bind`:

```csharp
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));

try
{
var form = Prompt.Bind<MyFormModel>(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation timed out or was canceled.");
}
```
60 changes: 49 additions & 11 deletions docs/en/prompt-types/bind.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,42 @@
# Bind (Model-based Prompts)

The `Bind` prompt automatically generates prompts based on a model class using data annotations.
The `Bind` prompt automatically generates a sequence of prompts based on a model class.

## Basic Usage

Define a model class with data annotations:
Define a model class with `[PromptBindable]` and data annotations:

```csharp
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Sharprompt;

[PromptBindable]
public class MyFormModel
{
[Display(Name = "What's your name?")]
[Required]
public string Name { get; set; }
public string Name { get; set; } = null!;

[Display(Name = "Type new password")]
[DataType(DataType.Password)]
[Required]
[MinLength(8)]
public string Password { get; set; }
public string Password { get; set; } = null!;

[Display(Name = "Select your city")]
[Required]
[InlineItems("Seattle", "London", "Tokyo")]
public string City { get; set; }
public string City { get; set; } = null!;

[Display(Name = "Select favorite fruit")]
[MemberItems(nameof(Fruits))]
public string Fruit { get; set; } = null!;

[Display(Name = "Are you ready?")]
public bool? Ready { get; set; }

public static IEnumerable<string> Fruits => new[] { "Apple", "Banana", "Orange" };
}
```

Expand All @@ -37,26 +46,55 @@ Then use `Prompt.Bind` to generate prompts for the model:
var result = Prompt.Bind<MyFormModel>();
```

## Model Requirements

To work with source-generated binding (including Native AOT):
- The model must be declared as a `class`.
- The class must have the `[PromptBindable]` attribute so the Roslyn source generator generates a binder at compile time.
- The class must have a public parameterless constructor when using `Prompt.Bind<T>()`. Alternatively, pass an existing instance to `Prompt.Bind(model)`.
- Bound properties must be public instance properties with accessible setters (`public`, `internal`, or `protected internal`). Init-only or read-only properties are skipped.

## How It Works

Sharprompt automatically determines the prompt type based on the property type and attributes:
Sharprompt automatically determines the prompt type based on property types and attributes:

| Property Type / Attribute | Prompt Type |
|--------------------------|-------------|
| `string` | Input |
| `string` with `[DataType(DataType.Password)]` | Password |
| `bool?` | Confirm |
| `string` with `[InlineItems(...)]` | Select |
| Enum type | Select |
| `bool` or `bool?` | Confirm |
| Single value with `[InlineItems(...)]` or `[MemberItems(...)]` | Select |
| `IEnumerable<T>` with `[InlineItems(...)]` or `[MemberItems(...)]` | MultiSelect |
| Enum type | Select (or MultiSelect if collection) |
| `IEnumerable<T>` (without items attribute) | List |

## Supported Attributes

| Attribute | Description |
|-----------|-------------|
| `[Display(Name = "...")]` | Sets the prompt message |
| `[PromptBindable]` | Marks the class for source-generated model binding |
| `[Display(Name = "...")]` | Sets prompt message (`Name`), placeholder (`Prompt`), and order (`Order`) |
| `[Required]` | Makes the field required |
| `[MinLength(n)]` | Sets minimum length validation |
| `[MaxLength(n)]` | Sets maximum length validation |
| `[DataType(DataType.Password)]` | Uses password prompt |
| `[InlineItems("a", "b", "c")]` | Provides items for select prompt |
| `[InlineItems("a", "b", "c")]` | Provides static inline items for selection |
| `[MemberItems(nameof(Member))]` | Provides items dynamically from a static property or method |
| `[BindIgnore]` | Skips the property during binding |

## Cancellation

You can pass a `CancellationToken` to cancel model binding at any point:

```csharp
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));

try
{
var result = Prompt.Bind<MyFormModel>(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Prompt binding was canceled.");
}
```
76 changes: 76 additions & 0 deletions docs/ja/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,79 @@ var city = Prompt.Select(new SelectOptions<string>
LoopingSelection = false
});
```

## Native AOT とソースジェネレーター

Sharprompt は `<IsAotCompatible>true</IsAotCompatible>` に対応しており、.NET 8 および .NET 10 で Native AOT コンパイルをサポートしています。

### 仕組み

Sharprompt は Roslyn インクリメンタルソースジェネレーターを利用してコンパイル時にコードを生成し、実行時のリフレクションを排除しています:

1. **`[PromptBindable]` ジェネレーター**: `[PromptBindable]` 属性が付与されたクラスを解析し、C# の `[ModuleInitializer]` 経由で登録される型安全なモデルバインダーコードを生成します。`Prompt.Bind<T>()` の呼び出し時、リフレクションなしで事前コンパイルされたバインダーが直接実行されます。
2. **Enum メタデータジェネレーター**: プロジェクト内の enum 宣言を解析し、値の一覧や `[Display]` 属性(表示名・並び順)を `[ModuleInitializer]` 経由で登録するコードを生成します。

### プロジェクト構成と発行

Native AOT でアプリケーションを発行するには、プロジェクトファイルに `<PublishAot>true</PublishAot>` を追加します:

```xml
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<PublishAot>true</PublishAot>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Sharprompt" Version="..." />
</ItemGroup>

</Project>
```

対象のランタイムを指定して `dotnet publish` コマンドを実行します:

```sh
dotnet publish -c Release -r win-x64
```

※ `win-x64` はターゲットのランタイム識別子(例: `linux-x64`、`osx-arm64`)に適宜置き換えてください。

### Enum の取り扱いと外部アセンブリ

- **同一プロジェクト内の Enum**: 同一プロジェクト内で宣言された enum は、コンパイル時にソースジェネレーターによって自動検出されます。`[Display]` で指定された表示名や並び順もコードとして直接生成されます。
- **外部アセンブリの Enum**: 外部アセンブリの enum(BCL の `DayOfWeek` や参照パッケージの enum など)はソースジェネレーターの解析対象外です。実行時、Sharprompt は Native AOT に配慮したリフレクションフォールバックを使用します。リフレクションを完全に排除したい場合や表示をカスタマイズしたい場合は、アプリケーション起動時に手動で登録できます:

```csharp
using Sharprompt;

EnumMetadataRegistry.Register<DayOfWeek>(
Enum.GetValues<DayOfWeek>(),
day => day.ToString()
);
```

### 型変換とカスタム型

プロンプト入力の型変換は `TypeDescriptor` コンバーターを利用しています。組み込みの標準コンバーター(`string`、数値型、`bool`、`char`、`Guid`、`DateTime`、enum、およびこれらの Nullable)は Native AOT 環境でも完全に保持されます。

`[TypeConverter]` 属性を持つ独自のカスタム型を使用する場合は、トリミングによってコンバーター型が削除されないよう、アプリケーション側で型を保持する設定(`[DynamicallyAccessedMembers]` やトリマー記述ファイルなど)を行ってください。

### キャンセル

モデルバインディングを含むすべてのプロンプトで `CancellationToken` によるキャンセルをサポートしています。`Prompt.Bind` にトークンを渡します:

```csharp
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));

try
{
var form = Prompt.Bind<MyFormModel>(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("操作がタイムアウトまたはキャンセルされました。");
}
```
Loading