Pārlūkot izejas kodu

test(api-test): 新增 tipitaka-discussions 用例;客户端按新规格重生

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
visuddhinanda 1 dienu atpakaļ
vecāks
revīzija
7485a99432

+ 89 - 0
api-test/cases/09_tipitaka_discussions.php

@@ -0,0 +1,89 @@
+<?php
+
+use \WikipaliApi\Api\TipitakaDiscussionsApi;
+
+/*
+ * 按经文范围列讨论:GET /v3/tipitaka-discussions/{channel}?book=&chapter=|para=&to=&type=…
+ *
+ * 只读端点。完整一轮借 discussions 的写接口在 fixtures 的 discussion_sentence 上建一条
+ * issue(测试账号自己的句子,notify=false),按它所在的段落列出来核对,最后删掉。
+ * 权限矩阵(匿名 / basic 可见性)、排序、墓碑由 api-v13 的 TipitakaDiscussionV3Test 覆盖。
+ */
+
+/** fixture 句子的 book / paragraph(从 v2 单句接口取,各服务器的 fixture 不同) */
+function tipitaka_discussion_coord(): array
+{
+    $res = raw('GET', '/v2/sentence/' . fixtures()['discussion_sentence']);
+    assert_status(200, $res, '取 fixture 句子');
+    $data = $res['body']['data'] ?? [];
+
+    return [(int) ($data['book'] ?? 0), (int) ($data['paragraph'] ?? 0)];
+}
+
+test('GET /v3/tipitaka-discussions/{channel} → 200,列表 + 分页', function () {
+    $res = request(fn () => api(TipitakaDiscussionsApi::class)
+        ->getApiV3TipitakaDiscussionsChannelWithHttpInfo(fixtures()['discussion_channel']));
+    assert_status(200, $res, 'tipitaka-discussions');
+    assert_has_key('data', $res['body'], 'tipitaka-discussions');
+    assert_has_key('meta', $res['body'], 'tipitaka-discussions');
+});
+
+test('GET /v3/tipitaka-discussions/{channel}(chapter 不带 book)→ 422', function () {
+    $res = raw('GET', '/v3/tipitaka-discussions/' . fixtures()['discussion_channel'], ['chapter' => 1]);
+    assert_status(422, $res, 'chapter 不带 book');
+    assert_field_error('book', $res, 'chapter 不带 book');
+});
+
+test('GET /v3/tipitaka-discussions/{channel}(非法 type)→ 422', function () {
+    $res = raw('GET', '/v3/tipitaka-discussions/' . fixtures()['discussion_channel'], ['type' => 'issue,bogus']);
+    assert_status(422, $res, '非法 type');
+    assert_field_error('type', $res, '非法 type');
+});
+
+test('GET /v3/tipitaka-discussions/{非 uuid} → 404', function () {
+    assert_status(404, raw('GET', '/v3/tipitaka-discussions/not-a-uuid'), '非 uuid 的 channel');
+});
+
+test('tipitaka-discussions 一轮:在 fixture 句子上建 issue → 按段落列出、带句子坐标 → 按类型过滤 → 删', function () {
+    $token = required_token();
+    [$book, $para] = tipitaka_discussion_coord();
+    $stamp = 'api-test-tipitaka-discussions-' . date('Ymd-His');
+
+    $create = raw('POST', '/v3/discussions', null, [
+        'target_type' => 'sentence', 'target_id' => fixtures()['discussion_sentence'],
+        'type' => 'issue', 'severity' => 'suggestion', 'title' => $stamp,
+        'content' => '由 api-test 建立,测完即删', 'notify' => false,
+    ], $token);
+    assert_status(201, $create, '建 issue');
+    $id = $create['body']['data']['id'] ?? null;
+    if (! $id) {
+        fail('建 issue:应返回 id');
+    }
+
+    try {
+        $client = api(TipitakaDiscussionsApi::class, $token);
+        $list = request(fn () => $client->getApiV3TipitakaDiscussionsChannelWithHttpInfo(
+            fixtures()['discussion_channel'], 200, $book, null, $para, null, 'issue'));
+        assert_status(200, $list, '按段落列 issue');
+        $hit = null;
+        foreach ($list['body']['data'] ?? [] as $row) {
+            if (($row['id'] ?? null) === $id) {
+                $hit = $row;
+            }
+        }
+        if (! $hit) {
+            fail('按段落列 issue:应包含刚建的那条');
+        }
+        assert_eq($book, $hit['sentence']['book'] ?? null, '句子坐标 book');
+        assert_eq($para, $hit['sentence']['paragraph'] ?? null, '句子坐标 paragraph');
+
+        // 按别的类型过滤就不该出现
+        $glossary = request(fn () => $client->getApiV3TipitakaDiscussionsChannelWithHttpInfo(
+            fixtures()['discussion_channel'], 200, $book, null, $para, null, 'glossary'));
+        assert_status(200, $glossary, '按 glossary 过滤');
+        assert_eq(false, in_array($id, array_column($glossary['body']['data'] ?? [], 'id'), true), '按 glossary 过滤不含 issue');
+    } finally {
+        $del = raw('DELETE', "/v3/discussions/{$id}", null, null, $token);
+    }
+    assert_status(200, $del, '删 issue');
+});

+ 6 - 0
api-test/client/.openapi-generator/FILES

@@ -16,6 +16,7 @@ docs/Api/ReactionsApi.md
 docs/Api/SearchApi.md
 docs/Api/SearchApi.md
 docs/Api/SearchSuggestApi.md
 docs/Api/SearchSuggestApi.md
 docs/Api/SessionsApi.md
 docs/Api/SessionsApi.md
+docs/Api/TipitakaDiscussionsApi.md
 docs/Api/TipitakaReadingApi.md
 docs/Api/TipitakaReadingApi.md
 docs/Api/TipitakaRelatedParagraphsApi.md
 docs/Api/TipitakaRelatedParagraphsApi.md
 docs/Api/UpgradeApi.md
 docs/Api/UpgradeApi.md
@@ -24,6 +25,7 @@ docs/Model/GetApiV3Discussions200Response.md
 docs/Model/GetApiV3Discussions200ResponseDataInner.md
 docs/Model/GetApiV3Discussions200ResponseDataInner.md
 docs/Model/GetApiV3Discussions200ResponseDataInnerAbilities.md
 docs/Model/GetApiV3Discussions200ResponseDataInnerAbilities.md
 docs/Model/GetApiV3Discussions200ResponseDataInnerEditor.md
 docs/Model/GetApiV3Discussions200ResponseDataInnerEditor.md
+docs/Model/GetApiV3Discussions200ResponseDataInnerSentence.md
 docs/Model/GetApiV3DiscussionsSummary200Response.md
 docs/Model/GetApiV3DiscussionsSummary200Response.md
 docs/Model/GetApiV3DiscussionsSummary200ResponseData.md
 docs/Model/GetApiV3DiscussionsSummary200ResponseData.md
 docs/Model/GetApiV3DiscussionsSummary200ResponseDataStatusCounts.md
 docs/Model/GetApiV3DiscussionsSummary200ResponseDataStatusCounts.md
@@ -93,6 +95,7 @@ lib/Api/ReactionsApi.php
 lib/Api/SearchApi.php
 lib/Api/SearchApi.php
 lib/Api/SearchSuggestApi.php
 lib/Api/SearchSuggestApi.php
 lib/Api/SessionsApi.php
 lib/Api/SessionsApi.php
+lib/Api/TipitakaDiscussionsApi.php
 lib/Api/TipitakaReadingApi.php
 lib/Api/TipitakaReadingApi.php
 lib/Api/TipitakaRelatedParagraphsApi.php
 lib/Api/TipitakaRelatedParagraphsApi.php
 lib/Api/UpgradeApi.php
 lib/Api/UpgradeApi.php
@@ -105,6 +108,7 @@ lib/Model/GetApiV3Discussions200Response.php
 lib/Model/GetApiV3Discussions200ResponseDataInner.php
 lib/Model/GetApiV3Discussions200ResponseDataInner.php
 lib/Model/GetApiV3Discussions200ResponseDataInnerAbilities.php
 lib/Model/GetApiV3Discussions200ResponseDataInnerAbilities.php
 lib/Model/GetApiV3Discussions200ResponseDataInnerEditor.php
 lib/Model/GetApiV3Discussions200ResponseDataInnerEditor.php
+lib/Model/GetApiV3Discussions200ResponseDataInnerSentence.php
 lib/Model/GetApiV3DiscussionsSummary200Response.php
 lib/Model/GetApiV3DiscussionsSummary200Response.php
 lib/Model/GetApiV3DiscussionsSummary200ResponseData.php
 lib/Model/GetApiV3DiscussionsSummary200ResponseData.php
 lib/Model/GetApiV3DiscussionsSummary200ResponseDataStatusCounts.php
 lib/Model/GetApiV3DiscussionsSummary200ResponseDataStatusCounts.php
@@ -176,12 +180,14 @@ test/Api/ReactionsApiTest.php
 test/Api/SearchApiTest.php
 test/Api/SearchApiTest.php
 test/Api/SearchSuggestApiTest.php
 test/Api/SearchSuggestApiTest.php
 test/Api/SessionsApiTest.php
 test/Api/SessionsApiTest.php
+test/Api/TipitakaDiscussionsApiTest.php
 test/Api/TipitakaReadingApiTest.php
 test/Api/TipitakaReadingApiTest.php
 test/Api/TipitakaRelatedParagraphsApiTest.php
 test/Api/TipitakaRelatedParagraphsApiTest.php
 test/Api/UpgradeApiTest.php
 test/Api/UpgradeApiTest.php
 test/Api/UsersApiTest.php
 test/Api/UsersApiTest.php
 test/Model/GetApiV3Discussions200ResponseDataInnerAbilitiesTest.php
 test/Model/GetApiV3Discussions200ResponseDataInnerAbilitiesTest.php
 test/Model/GetApiV3Discussions200ResponseDataInnerEditorTest.php
 test/Model/GetApiV3Discussions200ResponseDataInnerEditorTest.php
+test/Model/GetApiV3Discussions200ResponseDataInnerSentenceTest.php
 test/Model/GetApiV3Discussions200ResponseDataInnerTest.php
 test/Model/GetApiV3Discussions200ResponseDataInnerTest.php
 test/Model/GetApiV3Discussions200ResponseTest.php
 test/Model/GetApiV3Discussions200ResponseTest.php
 test/Model/GetApiV3DiscussionsSummary200ResponseDataStatusCountsTest.php
 test/Model/GetApiV3DiscussionsSummary200ResponseDataStatusCountsTest.php

+ 636 - 0
api-test/client/lib/Api/TipitakaDiscussionsApi.php

@@ -0,0 +1,636 @@
+<?php
+/**
+ * TipitakaDiscussionsApi
+ * PHP version 8.1
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+
+/**
+ * MINT API
+ *
+ * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
+ *
+ * The version of the OpenAPI document: 2026.10.21
+ * Generated by: https://openapi-generator.tech
+ * Generator version: 7.25.0
+ */
+
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+namespace WikipaliApi\Api;
+
+use GuzzleHttp\Client;
+use GuzzleHttp\ClientInterface;
+use GuzzleHttp\Exception\ConnectException;
+use GuzzleHttp\Exception\RequestException;
+use GuzzleHttp\Psr7\MultipartStream;
+use GuzzleHttp\Psr7\Request;
+use GuzzleHttp\RequestOptions;
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ResponseInterface;
+use WikipaliApi\ApiException;
+use WikipaliApi\Configuration;
+use WikipaliApi\FormDataProcessor;
+use WikipaliApi\HeaderSelector;
+use WikipaliApi\ObjectSerializer;
+
+/**
+ * TipitakaDiscussionsApi Class Doc Comment
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+class TipitakaDiscussionsApi
+{
+    /**
+     * @var ClientInterface
+     */
+    protected $client;
+
+    /**
+     * @var Configuration
+     */
+    protected $config;
+
+    /**
+     * @var HeaderSelector
+     */
+    protected $headerSelector;
+
+    /**
+     * @var int Host index
+     */
+    protected $hostIndex;
+
+    /** @var string[] $contentTypes **/
+    public const contentTypes = [
+        'getApiV3TipitakaDiscussionsChannel' => [
+            'application/json',
+        ],
+    ];
+
+    /**
+     * @param ClientInterface $client
+     * @param Configuration   $config
+     * @param HeaderSelector  $selector
+     * @param int             $hostIndex (Optional) host index to select the list of hosts if defined in the OpenAPI spec
+     */
+    public function __construct(
+        ?ClientInterface $client = null,
+        ?Configuration $config = null,
+        ?HeaderSelector $selector = null,
+        int $hostIndex = 0
+    ) {
+        $this->client = $client ?: new Client();
+        $this->config = $config ?: Configuration::getDefaultConfiguration();
+        $this->headerSelector = $selector ?: new HeaderSelector();
+        $this->hostIndex = $hostIndex;
+    }
+
+    /**
+     * Set the host index
+     *
+     * @param int $hostIndex Host index (required)
+     */
+    public function setHostIndex($hostIndex): void
+    {
+        $this->hostIndex = $hostIndex;
+    }
+
+    /**
+     * Get the host index
+     *
+     * @return int Host index
+     */
+    public function getHostIndex()
+    {
+        return $this->hostIndex;
+    }
+
+    /**
+     * @return Configuration
+     */
+    public function getConfig()
+    {
+        return $this->config;
+    }
+
+    /**
+     * Operation getApiV3TipitakaDiscussionsChannel
+     *
+     * 列出一段经文里的话题
+     *
+     * @param  string $channel 句子所在 channel 的 uuid (required)
+     * @param  int|null $per_page 每页数量,最大 200 (optional, default to 15)
+     * @param  int|null $book 书号;与 chapter / para / to 一起用时必填,不传则是整个 channel (optional)
+     * @param  int|null $chapter 章节起始段落号,服务端按章节长度展开成段落区间;不能与 para / to 同用 (optional)
+     * @param  int|null $para 起始段落号 (optional)
+     * @param  int|null $to 结束段落号(含),缺省同 para (optional)
+     * @param  string|null $type 类型,逗号分隔多选;不传则只列评论线程 (optional)
+     * @param  string|null $status 话题状态,不传则两种都要 (optional)
+     * @param  string|null $severity 严重程度,逗号分隔多选 (optional)
+     * @param  int|null $page 页码 (optional, default to 1)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3TipitakaDiscussionsChannel'] to see the possible values for this operation
+     *
+     * @throws \WikipaliApi\ApiException on non-2xx response or if the response body is not in the expected format
+     * @throws \InvalidArgumentException
+     * @return \WikipaliApi\Model\GetApiV3Discussions200Response|\WikipaliApi\Model\ProblemDetails|\WikipaliApi\Model\ProblemDetails
+     */
+    public function getApiV3TipitakaDiscussionsChannel($channel, $per_page = 15, $book = null, $chapter = null, $para = null, $to = null, $type = null, $status = null, $severity = null, $page = 1, string $contentType = self::contentTypes['getApiV3TipitakaDiscussionsChannel'][0])
+    {
+        list($response) = $this->getApiV3TipitakaDiscussionsChannelWithHttpInfo($channel, $per_page, $book, $chapter, $para, $to, $type, $status, $severity, $page, $contentType);
+        return $response;
+    }
+
+    /**
+     * Operation getApiV3TipitakaDiscussionsChannelWithHttpInfo
+     *
+     * 列出一段经文里的话题
+     *
+     * @param  string $channel 句子所在 channel 的 uuid (required)
+     * @param  int|null $per_page 每页数量,最大 200 (optional, default to 15)
+     * @param  int|null $book 书号;与 chapter / para / to 一起用时必填,不传则是整个 channel (optional)
+     * @param  int|null $chapter 章节起始段落号,服务端按章节长度展开成段落区间;不能与 para / to 同用 (optional)
+     * @param  int|null $para 起始段落号 (optional)
+     * @param  int|null $to 结束段落号(含),缺省同 para (optional)
+     * @param  string|null $type 类型,逗号分隔多选;不传则只列评论线程 (optional)
+     * @param  string|null $status 话题状态,不传则两种都要 (optional)
+     * @param  string|null $severity 严重程度,逗号分隔多选 (optional)
+     * @param  int|null $page 页码 (optional, default to 1)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3TipitakaDiscussionsChannel'] to see the possible values for this operation
+     *
+     * @throws \WikipaliApi\ApiException on non-2xx response or if the response body is not in the expected format
+     * @throws \InvalidArgumentException
+     * @return array of \WikipaliApi\Model\GetApiV3Discussions200Response|\WikipaliApi\Model\ProblemDetails|\WikipaliApi\Model\ProblemDetails, HTTP status code, HTTP response headers (array of strings)
+     */
+    public function getApiV3TipitakaDiscussionsChannelWithHttpInfo($channel, $per_page = 15, $book = null, $chapter = null, $para = null, $to = null, $type = null, $status = null, $severity = null, $page = 1, string $contentType = self::contentTypes['getApiV3TipitakaDiscussionsChannel'][0])
+    {
+        $request = $this->getApiV3TipitakaDiscussionsChannelRequest($channel, $per_page, $book, $chapter, $para, $to, $type, $status, $severity, $page, $contentType);
+
+        try {
+            $options = $this->createHttpClientOption();
+            try {
+                $response = $this->client->send($request, $options);
+            } catch (RequestException $e) {
+                throw new ApiException(
+                    "[{$e->getCode()}] {$e->getMessage()}",
+                    (int) $e->getCode(),
+                    $e->getResponse() ? $e->getResponse()->getHeaders() : null,
+                    $e->getResponse() ? (string) $e->getResponse()->getBody() : null
+                );
+            } catch (ConnectException $e) {
+                throw new ApiException(
+                    "[{$e->getCode()}] {$e->getMessage()}",
+                    (int) $e->getCode(),
+                    null,
+                    null
+                );
+            }
+
+            $statusCode = $response->getStatusCode();
+
+
+            switch($statusCode) {
+                case 200:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\GetApiV3Discussions200Response',
+                        $request,
+                        $response,
+                    );
+                case 404:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $request,
+                        $response,
+                    );
+                case 422:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $request,
+                        $response,
+                    );
+            }
+
+            
+
+            if ($statusCode < 200 || $statusCode > 299) {
+                throw new ApiException(
+                    sprintf(
+                        '[%d] Error connecting to the API (%s)',
+                        $statusCode,
+                        (string) $request->getUri()
+                    ),
+                    $statusCode,
+                    $response->getHeaders(),
+                    (string) $response->getBody()
+                );
+            }
+
+            return $this->handleResponseWithDataType(
+                '\WikipaliApi\Model\GetApiV3Discussions200Response',
+                $request,
+                $response,
+            );
+        } catch (ApiException $e) {
+            switch ($e->getCode()) {
+                case 200:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\GetApiV3Discussions200Response',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+                case 404:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+                case 422:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+            }
+        
+
+            throw $e;
+        }
+    }
+
+    /**
+     * Operation getApiV3TipitakaDiscussionsChannelAsync
+     *
+     * 列出一段经文里的话题
+     *
+     * @param  string $channel 句子所在 channel 的 uuid (required)
+     * @param  int|null $per_page 每页数量,最大 200 (optional, default to 15)
+     * @param  int|null $book 书号;与 chapter / para / to 一起用时必填,不传则是整个 channel (optional)
+     * @param  int|null $chapter 章节起始段落号,服务端按章节长度展开成段落区间;不能与 para / to 同用 (optional)
+     * @param  int|null $para 起始段落号 (optional)
+     * @param  int|null $to 结束段落号(含),缺省同 para (optional)
+     * @param  string|null $type 类型,逗号分隔多选;不传则只列评论线程 (optional)
+     * @param  string|null $status 话题状态,不传则两种都要 (optional)
+     * @param  string|null $severity 严重程度,逗号分隔多选 (optional)
+     * @param  int|null $page 页码 (optional, default to 1)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3TipitakaDiscussionsChannel'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Promise\PromiseInterface
+     */
+    public function getApiV3TipitakaDiscussionsChannelAsync($channel, $per_page = 15, $book = null, $chapter = null, $para = null, $to = null, $type = null, $status = null, $severity = null, $page = 1, string $contentType = self::contentTypes['getApiV3TipitakaDiscussionsChannel'][0])
+    {
+        return $this->getApiV3TipitakaDiscussionsChannelAsyncWithHttpInfo($channel, $per_page, $book, $chapter, $para, $to, $type, $status, $severity, $page, $contentType)
+            ->then(
+                function ($response) {
+                    return $response[0];
+                }
+            );
+    }
+
+    /**
+     * Operation getApiV3TipitakaDiscussionsChannelAsyncWithHttpInfo
+     *
+     * 列出一段经文里的话题
+     *
+     * @param  string $channel 句子所在 channel 的 uuid (required)
+     * @param  int|null $per_page 每页数量,最大 200 (optional, default to 15)
+     * @param  int|null $book 书号;与 chapter / para / to 一起用时必填,不传则是整个 channel (optional)
+     * @param  int|null $chapter 章节起始段落号,服务端按章节长度展开成段落区间;不能与 para / to 同用 (optional)
+     * @param  int|null $para 起始段落号 (optional)
+     * @param  int|null $to 结束段落号(含),缺省同 para (optional)
+     * @param  string|null $type 类型,逗号分隔多选;不传则只列评论线程 (optional)
+     * @param  string|null $status 话题状态,不传则两种都要 (optional)
+     * @param  string|null $severity 严重程度,逗号分隔多选 (optional)
+     * @param  int|null $page 页码 (optional, default to 1)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3TipitakaDiscussionsChannel'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Promise\PromiseInterface
+     */
+    public function getApiV3TipitakaDiscussionsChannelAsyncWithHttpInfo($channel, $per_page = 15, $book = null, $chapter = null, $para = null, $to = null, $type = null, $status = null, $severity = null, $page = 1, string $contentType = self::contentTypes['getApiV3TipitakaDiscussionsChannel'][0])
+    {
+        $returnType = '\WikipaliApi\Model\GetApiV3Discussions200Response';
+        $request = $this->getApiV3TipitakaDiscussionsChannelRequest($channel, $per_page, $book, $chapter, $para, $to, $type, $status, $severity, $page, $contentType);
+
+        return $this->client
+            ->sendAsync($request, $this->createHttpClientOption())
+            ->then(
+                function ($response) use ($returnType) {
+                    if ($returnType === '\SplFileObject') {
+                        $content = $response->getBody(); //stream goes to serializer
+                    } else {
+                        $content = (string) $response->getBody();
+                        if ($returnType !== 'string') {
+                            $content = json_decode($content);
+                        }
+                    }
+
+                    return [
+                        ObjectSerializer::deserialize($content, $returnType, []),
+                        $response->getStatusCode(),
+                        $response->getHeaders()
+                    ];
+                },
+                function ($exception) {
+                    $response = $exception->getResponse();
+                    $statusCode = $response->getStatusCode();
+                    throw new ApiException(
+                        sprintf(
+                            '[%d] Error connecting to the API (%s)',
+                            $statusCode,
+                            $exception->getRequest()->getUri()
+                        ),
+                        $statusCode,
+                        $response->getHeaders(),
+                        (string) $response->getBody()
+                    );
+                }
+            );
+    }
+
+    /**
+     * Create request for operation 'getApiV3TipitakaDiscussionsChannel'
+     *
+     * @param  string $channel 句子所在 channel 的 uuid (required)
+     * @param  int|null $per_page 每页数量,最大 200 (optional, default to 15)
+     * @param  int|null $book 书号;与 chapter / para / to 一起用时必填,不传则是整个 channel (optional)
+     * @param  int|null $chapter 章节起始段落号,服务端按章节长度展开成段落区间;不能与 para / to 同用 (optional)
+     * @param  int|null $para 起始段落号 (optional)
+     * @param  int|null $to 结束段落号(含),缺省同 para (optional)
+     * @param  string|null $type 类型,逗号分隔多选;不传则只列评论线程 (optional)
+     * @param  string|null $status 话题状态,不传则两种都要 (optional)
+     * @param  string|null $severity 严重程度,逗号分隔多选 (optional)
+     * @param  int|null $page 页码 (optional, default to 1)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3TipitakaDiscussionsChannel'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Psr7\Request
+     */
+    public function getApiV3TipitakaDiscussionsChannelRequest($channel, $per_page = 15, $book = null, $chapter = null, $para = null, $to = null, $type = null, $status = null, $severity = null, $page = 1, string $contentType = self::contentTypes['getApiV3TipitakaDiscussionsChannel'][0])
+    {
+
+        // verify the required parameter 'channel' is set
+        if ($channel === null || (is_array($channel) && count($channel) === 0)) {
+            throw new \InvalidArgumentException(
+                'Missing the required parameter $channel when calling getApiV3TipitakaDiscussionsChannel'
+            );
+        }
+
+
+
+
+
+
+
+
+
+
+
+        $resourcePath = '/v3/tipitaka-discussions/{channel}';
+        $formParams = [];
+        $queryParams = [];
+        $headerParams = [];
+        $httpBody = '';
+        $multipart = false;
+
+        // query params
+        $queryParams = array_merge($queryParams, ObjectSerializer::toQueryValue(
+            $per_page,
+            'per_page', // param base name
+            'integer', // openApiType
+            'form', // style
+            true, // explode
+            false // required
+        ) ?? []);
+        // query params
+        $queryParams = array_merge($queryParams, ObjectSerializer::toQueryValue(
+            $book,
+            'book', // param base name
+            'integer', // openApiType
+            'form', // style
+            true, // explode
+            false // required
+        ) ?? []);
+        // query params
+        $queryParams = array_merge($queryParams, ObjectSerializer::toQueryValue(
+            $chapter,
+            'chapter', // param base name
+            'integer', // openApiType
+            'form', // style
+            true, // explode
+            false // required
+        ) ?? []);
+        // query params
+        $queryParams = array_merge($queryParams, ObjectSerializer::toQueryValue(
+            $para,
+            'para', // param base name
+            'integer', // openApiType
+            'form', // style
+            true, // explode
+            false // required
+        ) ?? []);
+        // query params
+        $queryParams = array_merge($queryParams, ObjectSerializer::toQueryValue(
+            $to,
+            'to', // param base name
+            'integer', // openApiType
+            'form', // style
+            true, // explode
+            false // required
+        ) ?? []);
+        // query params
+        $queryParams = array_merge($queryParams, ObjectSerializer::toQueryValue(
+            $type,
+            'type', // param base name
+            'string', // openApiType
+            'form', // style
+            true, // explode
+            false // required
+        ) ?? []);
+        // query params
+        $queryParams = array_merge($queryParams, ObjectSerializer::toQueryValue(
+            $status,
+            'status', // param base name
+            'string', // openApiType
+            'form', // style
+            true, // explode
+            false // required
+        ) ?? []);
+        // query params
+        $queryParams = array_merge($queryParams, ObjectSerializer::toQueryValue(
+            $severity,
+            'severity', // param base name
+            'string', // openApiType
+            'form', // style
+            true, // explode
+            false // required
+        ) ?? []);
+        // query params
+        $queryParams = array_merge($queryParams, ObjectSerializer::toQueryValue(
+            $page,
+            'page', // param base name
+            'integer', // openApiType
+            'form', // style
+            true, // explode
+            false // required
+        ) ?? []);
+
+
+        // path params
+        if ($channel !== null) {
+            $resourcePath = str_replace(
+                '{channel}',
+                ObjectSerializer::toPathValue($channel),
+                $resourcePath
+            );
+        }
+
+
+        $headers = $this->headerSelector->selectHeaders(
+            ['application/json', 'application/problem+json', ],
+            $contentType,
+            $multipart
+        );
+
+        // for model (json/xml)
+        if (count($formParams) > 0) {
+            if ($multipart) {
+                $multipartContents = [];
+                foreach ($formParams as $formParamName => $formParamValue) {
+                    $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];
+                    foreach ($formParamValueItems as $formParamValueItem) {
+                        $multipartContents[] = [
+                            'name' => $formParamName,
+                            'contents' => $formParamValueItem
+                        ];
+                    }
+                }
+                // for HTTP post (form)
+                $httpBody = new MultipartStream($multipartContents);
+
+            } elseif (stripos($headers['Content-Type'], 'application/json') !== false) {
+                # if Content-Type contains "application/json", json_encode the form parameters
+                try {
+                    $httpBody = json_encode($formParams, JSON_THROW_ON_ERROR);
+                } catch (\JsonException $e) {
+                    throw new \InvalidArgumentException('json_encode error: ' . $e->getMessage(), 0, $e);
+                }
+            } else {
+                // for HTTP post (form)
+                $httpBody = ObjectSerializer::buildQuery($formParams);
+            }
+        }
+
+        // this endpoint requires Bearer authentication (access token)
+        if (!empty($this->config->getAccessToken())) {
+            $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
+        }
+
+        $defaultHeaders = [];
+        if ($this->config->getUserAgent()) {
+            $defaultHeaders['User-Agent'] = $this->config->getUserAgent();
+        }
+
+        $headers = array_merge(
+            $defaultHeaders,
+            $headerParams,
+            $headers
+        );
+
+        $operationHost = $this->config->getHost();
+        $query = ObjectSerializer::buildQuery($queryParams);
+        return new Request(
+            'GET',
+            $operationHost . $resourcePath . ($query ? "?{$query}" : ''),
+            $headers,
+            $httpBody
+        );
+    }
+
+    /**
+     * Create http client option
+     *
+     * @throws \RuntimeException on file opening failure
+     * @return array of http client options
+     */
+    protected function createHttpClientOption()
+    {
+        $options = [];
+        if ($this->config->getDebug()) {
+            $options[RequestOptions::DEBUG] = fopen($this->config->getDebugFile(), 'a');
+            if (!$options[RequestOptions::DEBUG]) {
+                throw new \RuntimeException('Failed to open the debug file: ' . $this->config->getDebugFile());
+            }
+        }
+
+        if ($this->config->getCertFile()) {
+            $options[RequestOptions::CERT] = $this->config->getCertFile();
+        }
+
+        if ($this->config->getKeyFile()) {
+            $options[RequestOptions::SSL_KEY] = $this->config->getKeyFile();
+        }
+
+        return $options;
+    }
+
+    private function handleResponseWithDataType(
+        string $dataType,
+        RequestInterface $request,
+        ResponseInterface $response
+    ): array {
+        if ($dataType === '\SplFileObject') {
+            $content = $response->getBody(); //stream goes to serializer
+        } else {
+            $content = (string) $response->getBody();
+            if ($dataType !== 'string') {
+                try {
+                    $content = json_decode($content, false, 512, JSON_THROW_ON_ERROR);
+                } catch (\JsonException $exception) {
+                    throw new ApiException(
+                        sprintf(
+                            'Error JSON decoding server response (%s)',
+                            $request->getUri()
+                        ),
+                        $response->getStatusCode(),
+                        $response->getHeaders(),
+                        $content
+                    );
+                }
+            }
+        }
+
+        return [
+            ObjectSerializer::deserialize($content, $dataType, []),
+            $response->getStatusCode(),
+            $response->getHeaders()
+        ];
+    }
+
+    private function responseWithinRangeCode(
+        string $rangeCode,
+        int $statusCode
+    ): bool {
+        $left = (int) ($rangeCode[0].'00');
+        $right = (int) ($rangeCode[0].'99');
+
+        return $statusCode >= $left && $statusCode <= $right;
+    }
+}

+ 34 - 0
api-test/client/lib/Model/GetApiV3Discussions200ResponseDataInner.php

@@ -76,6 +76,7 @@ class GetApiV3Discussions200ResponseDataInner implements ModelInterface, ArrayAc
         'quote_prefix' => 'string',
         'quote_prefix' => 'string',
         'quote_suffix' => 'string',
         'quote_suffix' => 'string',
         'reply_count' => 'int',
         'reply_count' => 'int',
+        'sentence' => '\WikipaliApi\Model\GetApiV3Discussions200ResponseDataInnerSentence',
         'editor' => '\WikipaliApi\Model\GetApiV3Discussions200ResponseDataInnerEditor',
         'editor' => '\WikipaliApi\Model\GetApiV3Discussions200ResponseDataInnerEditor',
         'co_editor' => '\WikipaliApi\Model\GetApiV3Discussions200ResponseDataInnerEditor',
         'co_editor' => '\WikipaliApi\Model\GetApiV3Discussions200ResponseDataInnerEditor',
         'abilities' => '\WikipaliApi\Model\GetApiV3Discussions200ResponseDataInnerAbilities',
         'abilities' => '\WikipaliApi\Model\GetApiV3Discussions200ResponseDataInnerAbilities',
@@ -111,6 +112,7 @@ class GetApiV3Discussions200ResponseDataInner implements ModelInterface, ArrayAc
         'quote_prefix' => null,
         'quote_prefix' => null,
         'quote_suffix' => null,
         'quote_suffix' => null,
         'reply_count' => null,
         'reply_count' => null,
+        'sentence' => null,
         'editor' => null,
         'editor' => null,
         'co_editor' => null,
         'co_editor' => null,
         'abilities' => null,
         'abilities' => null,
@@ -144,6 +146,7 @@ class GetApiV3Discussions200ResponseDataInner implements ModelInterface, ArrayAc
         'quote_prefix' => true,
         'quote_prefix' => true,
         'quote_suffix' => true,
         'quote_suffix' => true,
         'reply_count' => false,
         'reply_count' => false,
+        'sentence' => false,
         'editor' => true,
         'editor' => true,
         'co_editor' => true,
         'co_editor' => true,
         'abilities' => false,
         'abilities' => false,
@@ -257,6 +260,7 @@ class GetApiV3Discussions200ResponseDataInner implements ModelInterface, ArrayAc
         'quote_prefix' => 'quote_prefix',
         'quote_prefix' => 'quote_prefix',
         'quote_suffix' => 'quote_suffix',
         'quote_suffix' => 'quote_suffix',
         'reply_count' => 'reply_count',
         'reply_count' => 'reply_count',
+        'sentence' => 'sentence',
         'editor' => 'editor',
         'editor' => 'editor',
         'co_editor' => 'co_editor',
         'co_editor' => 'co_editor',
         'abilities' => 'abilities',
         'abilities' => 'abilities',
@@ -290,6 +294,7 @@ class GetApiV3Discussions200ResponseDataInner implements ModelInterface, ArrayAc
         'quote_prefix' => 'setQuotePrefix',
         'quote_prefix' => 'setQuotePrefix',
         'quote_suffix' => 'setQuoteSuffix',
         'quote_suffix' => 'setQuoteSuffix',
         'reply_count' => 'setReplyCount',
         'reply_count' => 'setReplyCount',
+        'sentence' => 'setSentence',
         'editor' => 'setEditor',
         'editor' => 'setEditor',
         'co_editor' => 'setCoEditor',
         'co_editor' => 'setCoEditor',
         'abilities' => 'setAbilities',
         'abilities' => 'setAbilities',
@@ -323,6 +328,7 @@ class GetApiV3Discussions200ResponseDataInner implements ModelInterface, ArrayAc
         'quote_prefix' => 'getQuotePrefix',
         'quote_prefix' => 'getQuotePrefix',
         'quote_suffix' => 'getQuoteSuffix',
         'quote_suffix' => 'getQuoteSuffix',
         'reply_count' => 'getReplyCount',
         'reply_count' => 'getReplyCount',
+        'sentence' => 'getSentence',
         'editor' => 'getEditor',
         'editor' => 'getEditor',
         'co_editor' => 'getCoEditor',
         'co_editor' => 'getCoEditor',
         'abilities' => 'getAbilities',
         'abilities' => 'getAbilities',
@@ -407,6 +413,7 @@ class GetApiV3Discussions200ResponseDataInner implements ModelInterface, ArrayAc
         $this->setIfExists('quote_prefix', $data ?? [], null);
         $this->setIfExists('quote_prefix', $data ?? [], null);
         $this->setIfExists('quote_suffix', $data ?? [], null);
         $this->setIfExists('quote_suffix', $data ?? [], null);
         $this->setIfExists('reply_count', $data ?? [], null);
         $this->setIfExists('reply_count', $data ?? [], null);
+        $this->setIfExists('sentence', $data ?? [], null);
         $this->setIfExists('editor', $data ?? [], null);
         $this->setIfExists('editor', $data ?? [], null);
         $this->setIfExists('co_editor', $data ?? [], null);
         $this->setIfExists('co_editor', $data ?? [], null);
         $this->setIfExists('abilities', $data ?? [], null);
         $this->setIfExists('abilities', $data ?? [], null);
@@ -1040,6 +1047,33 @@ class GetApiV3Discussions200ResponseDataInner implements ModelInterface, ArrayAc
         return $this;
         return $this;
     }
     }
 
 
+    /**
+     * Gets sentence
+     *
+     * @return \WikipaliApi\Model\GetApiV3Discussions200ResponseDataInnerSentence|null
+     */
+    public function getSentence()
+    {
+        return $this->container['sentence'];
+    }
+
+    /**
+     * Sets sentence
+     *
+     * @param \WikipaliApi\Model\GetApiV3Discussions200ResponseDataInnerSentence|null $sentence sentence
+     *
+     * @return self
+     */
+    public function setSentence($sentence)
+    {
+        if (is_null($sentence)) {
+            throw new \InvalidArgumentException('non-nullable sentence cannot be null');
+        }
+        $this->container['sentence'] = $sentence;
+
+        return $this;
+    }
+
     /**
     /**
      * Gets editor
      * Gets editor
      *
      *

+ 512 - 0
api-test/client/lib/Model/GetApiV3Discussions200ResponseDataInnerSentence.php

@@ -0,0 +1,512 @@
+<?php
+/**
+ * GetApiV3Discussions200ResponseDataInnerSentence
+ *
+ * PHP version 8.1
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+
+/**
+ * MINT API
+ *
+ * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
+ *
+ * The version of the OpenAPI document: 2026.10.21
+ * Generated by: https://openapi-generator.tech
+ * Generator version: 7.25.0
+ */
+
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+namespace WikipaliApi\Model;
+
+use \ArrayAccess;
+use \WikipaliApi\ObjectSerializer;
+
+/**
+ * GetApiV3Discussions200ResponseDataInnerSentence Class Doc Comment
+ *
+ * @category Class
+ * @description 仅在部分查询口径下返回。
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ * @implements \ArrayAccess<string, mixed>
+ */
+class GetApiV3Discussions200ResponseDataInnerSentence implements ModelInterface, ArrayAccess, \JsonSerializable
+{
+    public const DISCRIMINATOR = null;
+
+    /**
+     * The original name of the model.
+     *
+     * @var string
+     */
+    protected static $openAPIModelName = 'get_api_v3_discussions_200_response_data_inner_sentence';
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @var string[]
+     */
+    protected static $openAPITypes = [
+        'book' => 'int',
+        'paragraph' => 'int',
+        'word_start' => 'int',
+        'word_end' => 'int'
+    ];
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @var string[]
+     * @phpstan-var array<string, string|null>
+     * @psalm-var array<string, string|null>
+     */
+    protected static $openAPIFormats = [
+        'book' => null,
+        'paragraph' => null,
+        'word_start' => null,
+        'word_end' => null
+    ];
+
+    /**
+     * Array of nullable properties. Used for (de)serialization
+     *
+     * @var boolean[]
+     */
+    protected static array $openAPINullables = [
+        'book' => false,
+        'paragraph' => false,
+        'word_start' => false,
+        'word_end' => false
+    ];
+
+    /**
+     * If a nullable field gets set to null, insert it here
+     *
+     * @var boolean[]
+     */
+    protected array $openAPINullablesSetToNull = [];
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPITypes()
+    {
+        return self::$openAPITypes;
+    }
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPIFormats()
+    {
+        return self::$openAPIFormats;
+    }
+
+    /**
+     * Array of nullable properties
+     *
+     * @return array
+     */
+    protected static function openAPINullables(): array
+    {
+        return self::$openAPINullables;
+    }
+
+    /**
+     * Array of nullable field names deliberately set to null
+     *
+     * @return boolean[]
+     */
+    private function getOpenAPINullablesSetToNull(): array
+    {
+        return $this->openAPINullablesSetToNull;
+    }
+
+    /**
+     * Setter - Array of nullable field names deliberately set to null
+     *
+     * @param boolean[] $openAPINullablesSetToNull
+     */
+    private function setOpenAPINullablesSetToNull(array $openAPINullablesSetToNull): void
+    {
+        $this->openAPINullablesSetToNull = $openAPINullablesSetToNull;
+    }
+
+    /**
+     * Checks if a property is nullable
+     *
+     * @param string $property
+     * @return bool
+     */
+    public static function isNullable(string $property): bool
+    {
+        return self::openAPINullables()[$property] ?? false;
+    }
+
+    /**
+     * Checks if a nullable property is set to null.
+     *
+     * @param string $property
+     * @return bool
+     */
+    public function isNullableSetToNull(string $property): bool
+    {
+        return in_array($property, $this->getOpenAPINullablesSetToNull(), true);
+    }
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @var string[]
+     */
+    protected static $attributeMap = [
+        'book' => 'book',
+        'paragraph' => 'paragraph',
+        'word_start' => 'word_start',
+        'word_end' => 'word_end'
+    ];
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @var string[]
+     */
+    protected static $setters = [
+        'book' => 'setBook',
+        'paragraph' => 'setParagraph',
+        'word_start' => 'setWordStart',
+        'word_end' => 'setWordEnd'
+    ];
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @var string[]
+     */
+    protected static $getters = [
+        'book' => 'getBook',
+        'paragraph' => 'getParagraph',
+        'word_start' => 'getWordStart',
+        'word_end' => 'getWordEnd'
+    ];
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @return array
+     */
+    public static function attributeMap()
+    {
+        return self::$attributeMap;
+    }
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @return array
+     */
+    public static function setters()
+    {
+        return self::$setters;
+    }
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @return array
+     */
+    public static function getters()
+    {
+        return self::$getters;
+    }
+
+    /**
+     * The original name of the model.
+     *
+     * @return string
+     */
+    public function getModelName()
+    {
+        return self::$openAPIModelName;
+    }
+
+
+    /**
+     * Associative array for storing property values
+     *
+     * @var mixed[]
+     */
+    protected $container = [];
+
+    /**
+     * Constructor
+     *
+     * @param mixed[]|null $data Associated array of property values
+     *                      initializing the model
+     */
+    public function __construct(?array $data = null)
+    {
+        $this->setIfExists('book', $data ?? [], null);
+        $this->setIfExists('paragraph', $data ?? [], null);
+        $this->setIfExists('word_start', $data ?? [], null);
+        $this->setIfExists('word_end', $data ?? [], null);
+    }
+
+    /**
+     * Sets $this->container[$variableName] to the given data or to the given default Value; if $variableName
+     * is nullable and its value is set to null in the $fields array, then mark it as "set to null" in the
+     * $this->openAPINullablesSetToNull array
+     *
+     * @param string $variableName
+     * @param array  $fields
+     * @param mixed  $defaultValue
+     */
+    private function setIfExists(string $variableName, array $fields, $defaultValue): void
+    {
+        if (self::isNullable($variableName) && array_key_exists($variableName, $fields) && is_null($fields[$variableName])) {
+            $this->openAPINullablesSetToNull[] = $variableName;
+        }
+
+        $this->container[$variableName] = $fields[$variableName] ?? $defaultValue;
+    }
+
+    /**
+     * Show all the invalid properties with reasons.
+     *
+     * @return array invalid properties with reasons
+     */
+    public function listInvalidProperties()
+    {
+        $invalidProperties = [];
+
+        return $invalidProperties;
+    }
+
+    /**
+     * Validate all the properties in the model
+     * return true if all passed
+     *
+     * @return bool True if all properties are valid
+     */
+    public function valid()
+    {
+        return count($this->listInvalidProperties()) === 0;
+    }
+
+
+    /**
+     * Gets book
+     *
+     * @return int|null
+     */
+    public function getBook()
+    {
+        return $this->container['book'];
+    }
+
+    /**
+     * Sets book
+     *
+     * @param int|null $book book
+     *
+     * @return self
+     */
+    public function setBook($book)
+    {
+        if (is_null($book)) {
+            throw new \InvalidArgumentException('non-nullable book cannot be null');
+        }
+        $this->container['book'] = $book;
+
+        return $this;
+    }
+
+    /**
+     * Gets paragraph
+     *
+     * @return int|null
+     */
+    public function getParagraph()
+    {
+        return $this->container['paragraph'];
+    }
+
+    /**
+     * Sets paragraph
+     *
+     * @param int|null $paragraph paragraph
+     *
+     * @return self
+     */
+    public function setParagraph($paragraph)
+    {
+        if (is_null($paragraph)) {
+            throw new \InvalidArgumentException('non-nullable paragraph cannot be null');
+        }
+        $this->container['paragraph'] = $paragraph;
+
+        return $this;
+    }
+
+    /**
+     * Gets word_start
+     *
+     * @return int|null
+     */
+    public function getWordStart()
+    {
+        return $this->container['word_start'];
+    }
+
+    /**
+     * Sets word_start
+     *
+     * @param int|null $word_start word_start
+     *
+     * @return self
+     */
+    public function setWordStart($word_start)
+    {
+        if (is_null($word_start)) {
+            throw new \InvalidArgumentException('non-nullable word_start cannot be null');
+        }
+        $this->container['word_start'] = $word_start;
+
+        return $this;
+    }
+
+    /**
+     * Gets word_end
+     *
+     * @return int|null
+     */
+    public function getWordEnd()
+    {
+        return $this->container['word_end'];
+    }
+
+    /**
+     * Sets word_end
+     *
+     * @param int|null $word_end word_end
+     *
+     * @return self
+     */
+    public function setWordEnd($word_end)
+    {
+        if (is_null($word_end)) {
+            throw new \InvalidArgumentException('non-nullable word_end cannot be null');
+        }
+        $this->container['word_end'] = $word_end;
+
+        return $this;
+    }
+    /**
+     * Returns true if offset exists. False otherwise.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return boolean
+     */
+    public function offsetExists(mixed $offset): bool
+    {
+        return isset($this->container[$offset]);
+    }
+
+    /**
+     * Gets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return mixed|null
+     */
+    #[\ReturnTypeWillChange]
+    public function offsetGet(mixed $offset)
+    {
+        return $this->container[$offset] ?? null;
+    }
+
+    /**
+     * Sets value based on offset.
+     *
+     * @param int|null $offset Offset
+     * @param mixed    $value  Value to be set
+     *
+     * @return void
+     */
+    public function offsetSet($offset, $value): void
+    {
+        if (is_null($offset)) {
+            $this->container[] = $value;
+        } else {
+            $this->container[$offset] = $value;
+        }
+    }
+
+    /**
+     * Unsets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return void
+     */
+    public function offsetUnset(mixed $offset): void
+    {
+        unset($this->container[$offset]);
+    }
+
+    /**
+     * Serializes the object to a value that can be serialized natively by json_encode().
+     * @link https://www.php.net/manual/en/jsonserializable.jsonserialize.php
+     *
+     * @return mixed Returns data which can be serialized by json_encode(), which is a value
+     * of any type other than a resource.
+     */
+    #[\ReturnTypeWillChange]
+    public function jsonSerialize()
+    {
+       return ObjectSerializer::sanitizeForSerialization($this);
+    }
+
+    /**
+     * Gets the string presentation of the object
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return json_encode(
+            ObjectSerializer::sanitizeForSerialization($this),
+            JSON_PRETTY_PRINT
+        );
+    }
+
+    /**
+     * Gets a header-safe presentation of the object
+     *
+     * @return string
+     */
+    public function toHeaderValue()
+    {
+        return json_encode(ObjectSerializer::sanitizeForSerialization($this));
+    }
+}
+
+