validators.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  1. # █ █ ▀ █▄▀ ▄▀█ █▀█ ▀
  2. # █▀█ █ █ █ █▀█ █▀▄ █
  3. # © Copyright 2022
  4. # https://t.me/hikariatama
  5. #
  6. # 🔒 Licensed under the GNU AGPLv3
  7. # 🌐 https://www.gnu.org/licenses/agpl-3.0.html
  8. import functools
  9. from typing import Any, Optional, Union as TypeUnion
  10. from . import utils
  11. import grapheme
  12. import re
  13. class ValidationError(Exception):
  14. """
  15. Is being raised when config value passed can't be converted properly
  16. Must be raised with string, describing why value is incorrect
  17. It will be shown in .config, if user tries to set incorrect value
  18. """
  19. class Validator:
  20. """
  21. Class used as validator of config value
  22. :param validator: Sync function, which raises `ValidationError` if passed
  23. value is incorrect (with explanation) and returns converted
  24. value if it is semantically correct.
  25. ⚠️ If validator returns `None`, value will always be set to `None`
  26. :param doc: Docstrings for this validator as string, or dict in format:
  27. {
  28. "en": "docstring",
  29. "ru": "докстрингом",
  30. "ua": "докстрiнгом",
  31. "jp": "ヒント",
  32. }
  33. Use instrumental case with lowercase
  34. :param _internal_id: Do not pass anything here, or things will break
  35. """
  36. def __init__(
  37. self,
  38. validator: callable,
  39. doc: TypeUnion[str, dict] = None,
  40. _internal_id: int = None,
  41. ):
  42. self.validate = validator
  43. if isinstance(doc, str):
  44. doc = {"en": doc, "ru": doc}
  45. self.doc = doc
  46. self.internal_id = _internal_id
  47. def _Boolean(value: Any, /) -> bool:
  48. true_cases = ["True", "true", "1", 1, True]
  49. false_cases = ["False", "false", "0", 0, False]
  50. if value not in true_cases + false_cases:
  51. raise ValidationError("Passed value must be a boolean")
  52. return value in true_cases
  53. def Boolean() -> Validator:
  54. """
  55. Any logical value to be passed
  56. `1`, `"1"` etc. will be automatically converted to bool
  57. """
  58. return Validator(
  59. _Boolean,
  60. {"en": "boolean", "ru": "логическим значением"},
  61. _internal_id="Boolean",
  62. )
  63. def _Integer(
  64. value: Any,
  65. /,
  66. *,
  67. digits: int,
  68. minimum: int,
  69. maximum: int,
  70. ) -> TypeUnion[int, None]:
  71. try:
  72. value = int(str(value).strip())
  73. except ValueError:
  74. raise ValidationError(f"Passed value ({value}) must be a number")
  75. if minimum is not None and value < minimum:
  76. raise ValidationError(f"Passed value ({value}) is lower than minimum one")
  77. if maximum is not None and value > maximum:
  78. raise ValidationError(f"Passed value ({value}) is greater than maximum one")
  79. if digits is not None and len(str(value)) != digits:
  80. raise ValidationError(
  81. f"The length of passed value ({value}) is incorrect "
  82. f"(Must be exactly {digits} digits)"
  83. )
  84. return value
  85. def Integer(
  86. *,
  87. digits: Optional[int] = None,
  88. minimum: Optional[int] = None,
  89. maximum: Optional[int] = None,
  90. ) -> Validator:
  91. """
  92. Checks whether passed argument is an integer value
  93. :param digits: Digits quantity, which must be passed
  94. :param minimum: Minimal number to be passed
  95. :param maximum: Maximum number to be passed
  96. """
  97. _sign_en = "positive " if minimum is not None and minimum == 0 else ""
  98. _sign_ru = "положительным " if minimum is not None and minimum == 0 else ""
  99. _sign_en = "negative " if maximum is not None and maximum == 0 else _sign_en
  100. _sign_ru = "отрицательным " if maximum is not None and maximum == 0 else _sign_ru
  101. _digits_en = f" with exactly {digits} digits" if digits is not None else ""
  102. _digits_ru = f", в котором ровно {digits} цифр " if digits is not None else ""
  103. if minimum is not None and minimum != 0:
  104. doc = (
  105. {
  106. "en": f"{_sign_en}integer greater than {minimum}{_digits_en}",
  107. "ru": f"{_sign_ru}целым числом больше {minimum}{_digits_ru}",
  108. }
  109. if maximum is None and maximum != 0
  110. else {
  111. "en": f"{_sign_en}integer from {minimum} to {maximum}{_digits_en}",
  112. "ru": (
  113. f"{_sign_ru}целым числом в промежутке от {minimum} до"
  114. f" {maximum}{_digits_ru}"
  115. ),
  116. }
  117. )
  118. elif maximum is None and maximum != 0:
  119. doc = {
  120. "en": f"{_sign_en}integer{_digits_en}",
  121. "ru": f"{_sign_ru}целым числом{_digits_ru}",
  122. }
  123. else:
  124. doc = {
  125. "en": f"{_sign_en}integer less than {maximum}{_digits_en}",
  126. "ru": f"{_sign_ru}целым числом меньше {maximum}{_digits_ru}",
  127. }
  128. return Validator(
  129. functools.partial(
  130. _Integer,
  131. digits=digits,
  132. minimum=minimum,
  133. maximum=maximum,
  134. ),
  135. doc,
  136. _internal_id="Integer",
  137. )
  138. def _Choice(value: Any, /, *, possible_values: list) -> Any:
  139. if value not in possible_values:
  140. raise ValidationError(
  141. f"Passed value ({value}) is not one of the following:"
  142. f" {'/'.join(list(map(str, possible_values)))}"
  143. )
  144. return value
  145. def Choice(possible_values: list, /) -> Validator:
  146. """
  147. Check whether entered value is in the allowed list
  148. :param possible_values: Allowed values to be passed to config param
  149. """
  150. return Validator(
  151. functools.partial(_Choice, possible_values=possible_values),
  152. {
  153. "en": f"one of the following: {'/'.join(list(map(str, possible_values)))}",
  154. "ru": f"одним из: {'/'.join(list(map(str, possible_values)))}",
  155. },
  156. _internal_id="Choice",
  157. )
  158. def _Series(
  159. value: Any,
  160. /,
  161. *,
  162. validator: Optional[Validator],
  163. min_len: Optional[int],
  164. max_len: Optional[int],
  165. fixed_len: Optional[int],
  166. ):
  167. if not isinstance(value, (list, tuple, set)):
  168. value = str(value).split(",")
  169. if isinstance(value, (tuple, set)):
  170. value = list(value)
  171. if min_len is not None and len(value) < min_len:
  172. raise ValidationError(
  173. f"Passed value ({value}) contains less than {min_len} items"
  174. )
  175. if max_len is not None and len(value) > max_len:
  176. raise ValidationError(
  177. f"Passed value ({value}) contains more than {max_len} items"
  178. )
  179. if fixed_len is not None and len(value) != fixed_len:
  180. raise ValidationError(
  181. f"Passed value ({value}) must contain exactly {fixed_len} items"
  182. )
  183. value = [item.strip() if isinstance(item, str) else item for item in value]
  184. if isinstance(validator, Validator):
  185. for i, item in enumerate(value):
  186. try:
  187. value[i] = validator.validate(item)
  188. except ValidationError:
  189. raise ValidationError(
  190. f"Passed value ({value}) contains invalid item"
  191. f" ({str(item).strip()}), which must be {validator.doc['en']}"
  192. )
  193. value = list(filter(lambda x: x, value))
  194. return value
  195. def Series(
  196. validator: Optional[Validator] = None,
  197. min_len: Optional[int] = None,
  198. max_len: Optional[int] = None,
  199. fixed_len: Optional[int] = None,
  200. ) -> Validator:
  201. """
  202. Represents the series of value (simply `list`)
  203. :param separator: With which separator values must be separated
  204. :param validator: Internal validator for each sequence value
  205. :param min_len: Minimal number of series items to be passed
  206. :param max_len: Maximum number of series items to be passed
  207. :param fixed_len: Fixed number of series items to be passed
  208. """
  209. _each_en = f" (each must be {validator.doc['en']})" if validator is not None else ""
  210. _each_ru = (
  211. f" (каждое должно быть {validator.doc['ru']})" if validator is not None else ""
  212. )
  213. if fixed_len is not None:
  214. _len_en = f" (exactly {fixed_len} pcs.)"
  215. _len_ru = f" (ровно {fixed_len} шт.)"
  216. elif min_len is None:
  217. if max_len is None:
  218. _len_en = ""
  219. _len_ru = ""
  220. else:
  221. _len_en = f" (up to {max_len} pcs.)"
  222. _len_ru = f" (до {max_len} шт.)"
  223. elif max_len is not None:
  224. _len_en = f" (from {min_len} to {max_len} pcs.)"
  225. _len_ru = f" (от {min_len} до {max_len} шт.)"
  226. else:
  227. _len_en = f" (at least {min_len} pcs.)"
  228. _len_ru = f" (как минимум {min_len} шт.)"
  229. doc = {
  230. "en": f"series of values{_len_en}{_each_en}, separated with «,»",
  231. "ru": f"списком значений{_len_ru}{_each_ru}, разделенных «,»",
  232. }
  233. return Validator(
  234. functools.partial(
  235. _Series,
  236. validator=validator,
  237. min_len=min_len,
  238. max_len=max_len,
  239. fixed_len=fixed_len,
  240. ),
  241. doc,
  242. _internal_id="Series",
  243. )
  244. def _Link(value: Any, /) -> str:
  245. try:
  246. if not utils.check_url(value):
  247. raise Exception("Invalid URL")
  248. except Exception:
  249. raise ValidationError(f"Passed value ({value}) is not a valid URL")
  250. return value
  251. def Link() -> Validator:
  252. """Valid url must be specified"""
  253. return Validator(
  254. lambda value: _Link(value),
  255. {
  256. "en": "link",
  257. "ru": "ссылкой",
  258. },
  259. _internal_id="Link",
  260. )
  261. def _String(value: Any, /, *, length: int) -> str:
  262. if isinstance(length, int) and len(list(grapheme.graphemes(str(value)))) != length:
  263. raise ValidationError(f"Passed value ({value}) must be a length of {length}")
  264. return str(value)
  265. def String(length: Optional[int] = None) -> Validator:
  266. """
  267. Checks for length of passed value and automatically converts it to string
  268. :param length: Exact length of string
  269. """
  270. if length is not None:
  271. doc = {
  272. "en": f"string of length {length}",
  273. "ru": f"строкой из {length} символа(-ов)",
  274. }
  275. else:
  276. doc = {
  277. "en": "string",
  278. "ru": "строкой",
  279. }
  280. return Validator(
  281. functools.partial(_String, length=length),
  282. doc,
  283. _internal_id="String",
  284. )
  285. def _RegExp(value: Any, /, *, regex: str) -> str:
  286. if not re.match(regex, value):
  287. raise ValidationError(f"Passed value ({value}) must follow pattern {regex}")
  288. return value
  289. def RegExp(regex: str) -> Validator:
  290. """
  291. Checks if value matches the regex
  292. :param regex: Regex to match
  293. """
  294. try:
  295. re.compile(regex)
  296. except re.error as e:
  297. raise Exception(f"{regex} is not a valid regex") from e
  298. doc = {
  299. "en": f"string matching pattern «{regex}»",
  300. "ru": f"строкой, соответствующей шаблону «{regex}»",
  301. }
  302. return Validator(
  303. functools.partial(_RegExp, regex=regex),
  304. doc,
  305. _internal_id="RegExp",
  306. )
  307. def _Float(
  308. value: Any,
  309. /,
  310. *,
  311. minimum: Optional[float] = None,
  312. maximum: Optional[float] = None,
  313. ) -> TypeUnion[int, None]:
  314. try:
  315. value = float(str(value).strip().replace(",", "."))
  316. except ValueError:
  317. raise ValidationError(f"Passed value ({value}) must be a float")
  318. if minimum is not None and value < minimum:
  319. raise ValidationError(f"Passed value ({value}) is lower than minimum one")
  320. if maximum is not None and value > maximum:
  321. raise ValidationError(f"Passed value ({value}) is greater than maximum one")
  322. return value
  323. def Float(
  324. minimum: Optional[float] = None,
  325. maximum: Optional[float] = None,
  326. ) -> Validator:
  327. """
  328. Checks whether passed argument is a float value
  329. :param minimum: Minimal number to be passed
  330. :param maximum: Maximum number to be passed
  331. """
  332. _sign_en = "positive " if minimum is not None and minimum == 0 else ""
  333. _sign_ru = "положительным " if minimum is not None and minimum == 0 else ""
  334. _sign_en = "negative " if maximum is not None and maximum == 0 else _sign_en
  335. _sign_ru = "отрицательным " if maximum is not None and maximum == 0 else _sign_ru
  336. if minimum is not None and minimum != 0:
  337. doc = (
  338. {
  339. "en": f"{_sign_en}float greater than {minimum}",
  340. "ru": f"{_sign_ru}дробным числом больше {minimum}",
  341. }
  342. if maximum is None and maximum != 0
  343. else {
  344. "en": f"{_sign_en}float from {minimum} to {maximum}",
  345. "ru": (
  346. f"{_sign_ru}дробным числом в промежутке от {minimum} до {maximum}"
  347. ),
  348. }
  349. )
  350. elif maximum is None and maximum != 0:
  351. doc = {
  352. "en": f"{_sign_en}float",
  353. "ru": f"{_sign_ru}дробным числом",
  354. }
  355. else:
  356. doc = {
  357. "en": f"{_sign_en}float less than {maximum}",
  358. "ru": f"{_sign_ru}дробным числом меньше {maximum}",
  359. }
  360. return Validator(
  361. functools.partial(
  362. _Float,
  363. minimum=minimum,
  364. maximum=maximum,
  365. ),
  366. doc,
  367. _internal_id="Float",
  368. )
  369. def _TelegramID(value: Any, /):
  370. e = ValidationError(f"Passed value ({value}) is not a valid telegram id")
  371. try:
  372. value = int(str(value).strip())
  373. except Exception:
  374. raise e
  375. if str(value).startswith("-100"):
  376. value = int(str(value)[4:])
  377. if value > 2**32 - 1 or value < 0:
  378. raise e
  379. return value
  380. def TelegramID() -> Validator:
  381. return Validator(
  382. _TelegramID,
  383. "Telegram ID",
  384. _internal_id="TelegramID",
  385. )
  386. def _Union(value: Any, /, *, validators: list):
  387. for validator in validators:
  388. try:
  389. return validator.validate(value)
  390. except ValidationError:
  391. pass
  392. raise ValidationError(f"Passed value ({value}) is not valid")
  393. def Union(*validators) -> Validator:
  394. doc = {
  395. "en": "one of the following:\n",
  396. "ru": "одним из следующего:\n",
  397. }
  398. def case(x: str) -> str:
  399. return x[0].upper() + x[1:]
  400. for validator in validators:
  401. doc["en"] += f"- {case(validator.doc['en'])}\n"
  402. doc["ru"] += f"- {case(validator.doc['ru'])}\n"
  403. doc["en"] = doc["en"].strip()
  404. doc["ru"] = doc["ru"].strip()
  405. return Validator(
  406. functools.partial(_Union, validators=validators),
  407. doc,
  408. _internal_id="Union",
  409. )
  410. def _NoneType(value: Any, /) -> None:
  411. if value not in {None, False, ""}:
  412. raise ValidationError(f"Passed value ({value}) is not None")
  413. return None
  414. def NoneType() -> Validator:
  415. return Validator(
  416. _NoneType,
  417. "`None`",
  418. _internal_id="NoneType",
  419. )
  420. def _Hidden(value: Any, /, *, validator: Validator) -> Any:
  421. return validator.validate(value)
  422. def Hidden(validator: Optional[Validator] = None) -> Validator:
  423. if not validator:
  424. validator = String()
  425. return Validator(
  426. functools.partial(_Hidden, validator=validator),
  427. validator.doc,
  428. _internal_id="Hidden",
  429. )