Skip to content

Commit da1d024

Browse files
Fix bug in handling datetimes in examples (#25)
1 parent f879a92 commit da1d024

File tree

6 files changed

+166
-8
lines changed

6 files changed

+166
-8
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.0.6] - 2023-03-18
9+
- Fixes a bug happening when trying to serialize examples in JSON, when they
10+
contain datetimes and are provided in YAML.
11+
([bug report](https://github.com/Neoteroi/mkdocs-plugins/issues/35)).
12+
813
## [1.0.5] - 2022-12-22 :santa:
914
- Fixes [#22](https://github.com/Neoteroi/essentials-openapi/issues/22)
1015

openapidocs/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
VERSION = "1.0.5"
1+
VERSION = "1.0.6"

openapidocs/common.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import base64
22
import copy
3-
import json
43
from abc import ABC, abstractmethod
54
from dataclasses import asdict, fields, is_dataclass
65
from datetime import date, datetime, time
@@ -9,7 +8,9 @@
98
from uuid import UUID
109

1110
import yaml
12-
from essentials.json import FriendlyEncoder
11+
from essentials.json import dumps
12+
13+
from openapidocs.mk.contents import OADJSONEncoder
1314

1415

1516
class Format(Enum):
@@ -150,9 +151,7 @@ def to_obj(self, item: Any) -> Any:
150151
return self._get_item_dictionary(item)
151152

152153
def to_json(self, item: Any) -> str:
153-
return json.dumps(
154-
self.to_obj(item), indent=4, ensure_ascii=False, cls=FriendlyEncoder
155-
)
154+
return dumps(self.to_obj(item), indent=4, cls=OADJSONEncoder)
156155

157156
def to_yaml(self, item: Any) -> str:
158157
rep = yaml.dump(

openapidocs/mk/contents.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,28 @@
11
"""
22
This module contains classes to generate representations of content types by mime type.
33
"""
4-
import json
4+
import os
55
from abc import ABC, abstractmethod
6+
from datetime import datetime
7+
from json import JSONEncoder
68
from urllib.parse import urlencode
79

10+
from essentials.json import FriendlyEncoder, dumps
11+
12+
13+
class OADJSONEncoder(JSONEncoder):
14+
def default(self, obj):
15+
try:
16+
return JSONEncoder.default(self, obj)
17+
except TypeError:
18+
if isinstance(obj, datetime):
19+
datetime_format = os.environ.get("OPENAPI_DATETIME_FORMAT")
20+
if datetime_format:
21+
return obj.strftime(datetime_format)
22+
else:
23+
return obj.isoformat()
24+
return FriendlyEncoder.default(self, obj) # type: ignore
25+
826

927
class ContentWriter(ABC):
1028
"""
@@ -30,7 +48,7 @@ def handle_content_type(self, content_type: str) -> bool:
3048
return "json" in content_type.lower()
3149

3250
def write(self, value) -> str:
33-
return json.dumps(value, ensure_ascii=False, indent=4)
51+
return dumps(value, indent=4, cls=OADJSONEncoder)
3452

3553

3654
class FormContentWriter(ContentWriter):

tests/test_mk.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
from datetime import date
2+
13
import pytest
24

35
from openapidocs.mk import get_http_status_phrase, read_dict
46
from openapidocs.mk.common import is_array_schema, is_object_schema
7+
from openapidocs.mk.contents import JSONContentWriter
8+
from openapidocs.mk.md import normalize_link
59

610

711
def test_get_http_status_phrase():
@@ -34,3 +38,25 @@ def test_is_object_schema():
3438
assert is_object_schema(1) is False
3539
assert is_object_schema({}) is False
3640
assert is_object_schema({"type": "object", "properties": {}}) is True
41+
42+
43+
def test_normalize_link_raises():
44+
with pytest.raises(ValueError):
45+
normalize_link(None) # type: ignore
46+
47+
with pytest.raises(ValueError):
48+
normalize_link("")
49+
50+
51+
def test_content_writer_dates():
52+
writer = JSONContentWriter()
53+
54+
expected_value = """
55+
{
56+
"date": "1986-05-30"
57+
}
58+
""".strip()
59+
60+
value = writer.write({"date": date(1986, 5, 30)})
61+
62+
assert value == expected_value

tests/test_v3.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import os
12
from abc import abstractmethod
23
from dataclasses import dataclass
34
from datetime import date, datetime, time
@@ -7,9 +8,11 @@
78
from uuid import UUID
89

910
import pytest
11+
import yaml
1012
from pydantic import BaseModel
1113

1214
from openapidocs.common import Format, Serializer
15+
from openapidocs.mk.contents import JSONContentWriter
1316
from openapidocs.v3 import (
1417
APIKeySecurity,
1518
Callback,
@@ -2293,3 +2296,110 @@ def test_equality(example_type: Type[TestItem]) -> None:
22932296
one = example.get_instance()
22942297
two = example.get_instance()
22952298
assert one == two
2299+
2300+
2301+
def test_serialize_datetimes_examples():
2302+
"""
2303+
Tests serialization using the default formatter for datetime.
2304+
"""
2305+
writer = JSONContentWriter()
2306+
2307+
yaml_text = """
2308+
description: Start time stamp of the returned data interval
2309+
example: 2022-08-17T18:00:00Z
2310+
in: query
2311+
name: fromInstant
2312+
required: false
2313+
schema:
2314+
format: date-time
2315+
type: string
2316+
"""
2317+
data = yaml.safe_load(yaml_text)
2318+
json_text = writer.write(data)
2319+
2320+
expected_json = """
2321+
{
2322+
"description": "Start time stamp of the returned data interval",
2323+
"example": "2022-08-17T18:00:00+00:00",
2324+
"in": "query",
2325+
"name": "fromInstant",
2326+
"required": false,
2327+
"schema": {
2328+
"format": "date-time",
2329+
"type": "string"
2330+
}
2331+
}
2332+
""".strip()
2333+
2334+
assert json_text == expected_json
2335+
2336+
2337+
def test_serialize_datetimes_examples_exact_format():
2338+
writer = JSONContentWriter()
2339+
2340+
yaml_text = """
2341+
description: Start time stamp of the returned data interval
2342+
example: '2022-08-17T18:00:00Z'
2343+
in: query
2344+
name: fromInstant
2345+
required: false
2346+
schema:
2347+
format: date-time
2348+
type: string
2349+
"""
2350+
data = yaml.safe_load(yaml_text)
2351+
json_text = writer.write(data)
2352+
2353+
expected_json = """
2354+
{
2355+
"description": "Start time stamp of the returned data interval",
2356+
"example": "2022-08-17T18:00:00Z",
2357+
"in": "query",
2358+
"name": "fromInstant",
2359+
"required": false,
2360+
"schema": {
2361+
"format": "date-time",
2362+
"type": "string"
2363+
}
2364+
}
2365+
""".strip()
2366+
2367+
assert json_text == expected_json
2368+
2369+
2370+
def test_serialize_datetimes_examples_exact_format_env():
2371+
os.environ["OPENAPI_DATETIME_FORMAT"] = "%Y-%m-%dT%H:%M:%SZ"
2372+
2373+
try:
2374+
writer = JSONContentWriter()
2375+
2376+
yaml_text = """
2377+
description: Start time stamp of the returned data interval
2378+
example: 2022-08-17T18:00:00Z
2379+
in: query
2380+
name: fromInstant
2381+
required: false
2382+
schema:
2383+
format: date-time
2384+
type: string
2385+
"""
2386+
data = yaml.safe_load(yaml_text)
2387+
json_text = writer.write(data)
2388+
2389+
expected_json = """
2390+
{
2391+
"description": "Start time stamp of the returned data interval",
2392+
"example": "2022-08-17T18:00:00Z",
2393+
"in": "query",
2394+
"name": "fromInstant",
2395+
"required": false,
2396+
"schema": {
2397+
"format": "date-time",
2398+
"type": "string"
2399+
}
2400+
}
2401+
""".strip()
2402+
2403+
assert json_text == expected_json
2404+
finally:
2405+
os.environ["OPENAPI_DATETIME_FORMAT"] = ""

0 commit comments

Comments
 (0)