validators.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  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 re
  9. import grapheme
  10. import functools
  11. from typing import Any, Optional, Union as TypeUnion
  12. from . import utils
  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: Optional[TypeUnion[str, dict]] = None,
  40. _internal_id: Optional[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. class Boolean(Validator):
  48. """
  49. Any logical value to be passed
  50. `1`, `"1"` etc. will be automatically converted to bool
  51. """
  52. def __init__(self):
  53. super().__init__(
  54. self._validate,
  55. {"en": "boolean", "ru": "логическим значением"},
  56. _internal_id="Boolean",
  57. )
  58. @staticmethod
  59. def _validate(value: Any, /) -> bool:
  60. true_cases = ["True", "true", "1", 1, True]
  61. false_cases = ["False", "false", "0", 0, False]
  62. if value not in true_cases + false_cases:
  63. raise ValidationError("Passed value must be a boolean")
  64. return value in true_cases
  65. class Integer(Validator):
  66. """
  67. Checks whether passed argument is an integer value
  68. :param digits: Digits quantity, which must be passed
  69. :param minimum: Minimal number to be passed
  70. :param maximum: Maximum number to be passed
  71. """
  72. def __init__(
  73. self,
  74. *,
  75. digits: Optional[int] = None,
  76. minimum: Optional[int] = None,
  77. maximum: Optional[int] = None,
  78. ):
  79. _sign_en = "positive " if minimum is not None and minimum == 0 else ""
  80. _sign_ru = "положительным " if minimum is not None and minimum == 0 else ""
  81. _sign_en = "negative " if maximum is not None and maximum == 0 else _sign_en
  82. _sign_ru = (
  83. "отрицательным " if maximum is not None and maximum == 0 else _sign_ru
  84. )
  85. _digits_en = f" with exactly {digits} digits" if digits is not None else ""
  86. _digits_ru = f", в котором ровно {digits} цифр " if digits is not None else ""
  87. if minimum is not None and minimum != 0:
  88. doc = (
  89. {
  90. "en": f"{_sign_en}integer greater than {minimum}{_digits_en}",
  91. "ru": f"{_sign_ru}целым числом больше {minimum}{_digits_ru}",
  92. }
  93. if maximum is None and maximum != 0
  94. else {
  95. "en": f"{_sign_en}integer from {minimum} to {maximum}{_digits_en}",
  96. "ru": (
  97. f"{_sign_ru}целым числом в промежутке от {minimum} до"
  98. f" {maximum}{_digits_ru}"
  99. ),
  100. }
  101. )
  102. elif maximum is None and maximum != 0:
  103. doc = {
  104. "en": f"{_sign_en}integer{_digits_en}",
  105. "ru": f"{_sign_ru}целым числом{_digits_ru}",
  106. }
  107. else:
  108. doc = {
  109. "en": f"{_sign_en}integer less than {maximum}{_digits_en}",
  110. "ru": f"{_sign_ru}целым числом меньше {maximum}{_digits_ru}",
  111. }
  112. super().__init__(
  113. functools.partial(
  114. self._validate,
  115. digits=digits,
  116. minimum=minimum,
  117. maximum=maximum,
  118. ),
  119. doc,
  120. _internal_id="Integer",
  121. )
  122. @staticmethod
  123. def _validate(
  124. value: Any,
  125. /,
  126. *,
  127. digits: int,
  128. minimum: int,
  129. maximum: int,
  130. ) -> TypeUnion[int, None]:
  131. try:
  132. value = int(str(value).strip())
  133. except ValueError:
  134. raise ValidationError(f"Passed value ({value}) must be a number")
  135. if minimum is not None and value < minimum:
  136. raise ValidationError(f"Passed value ({value}) is lower than minimum one")
  137. if maximum is not None and value > maximum:
  138. raise ValidationError(f"Passed value ({value}) is greater than maximum one")
  139. if digits is not None and len(str(value)) != digits:
  140. raise ValidationError(
  141. f"The length of passed value ({value}) is incorrect "
  142. f"(Must be exactly {digits} digits)"
  143. )
  144. return value
  145. class Choice(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. def __init__(self, possible_values: list, /):
  151. super().__init__(
  152. functools.partial(self._validate, possible_values=possible_values),
  153. {
  154. "en": (
  155. f"one of the following: {'/'.join(list(map(str, possible_values)))}"
  156. ),
  157. "ru": f"одним из: {'/'.join(list(map(str, possible_values)))}",
  158. },
  159. _internal_id="Choice",
  160. )
  161. @staticmethod
  162. def _validate(value: Any, /, *, possible_values: list) -> Any:
  163. if value not in possible_values:
  164. raise ValidationError(
  165. f"Passed value ({value}) is not one of the following:"
  166. f" {'/'.join(list(map(str, possible_values)))}"
  167. )
  168. return value
  169. class Series(Validator):
  170. """
  171. Represents the series of value (simply `list`)
  172. :param separator: With which separator values must be separated
  173. :param validator: Internal validator for each sequence value
  174. :param min_len: Minimal number of series items to be passed
  175. :param max_len: Maximum number of series items to be passed
  176. :param fixed_len: Fixed number of series items to be passed
  177. """
  178. def __init__(
  179. self,
  180. validator: Optional[Validator] = None,
  181. min_len: Optional[int] = None,
  182. max_len: Optional[int] = None,
  183. fixed_len: Optional[int] = None,
  184. ):
  185. _each_en = (
  186. f" (each must be {validator.doc['en']})" if validator is not None else ""
  187. )
  188. _each_ru = (
  189. f" (каждое должно быть {validator.doc['ru']})"
  190. if validator is not None
  191. else ""
  192. )
  193. if fixed_len is not None:
  194. _len_en = f" (exactly {fixed_len} pcs.)"
  195. _len_ru = f" (ровно {fixed_len} шт.)"
  196. elif min_len is None:
  197. if max_len is None:
  198. _len_en = ""
  199. _len_ru = ""
  200. else:
  201. _len_en = f" (up to {max_len} pcs.)"
  202. _len_ru = f" (до {max_len} шт.)"
  203. elif max_len is not None:
  204. _len_en = f" (from {min_len} to {max_len} pcs.)"
  205. _len_ru = f" (от {min_len} до {max_len} шт.)"
  206. else:
  207. _len_en = f" (at least {min_len} pcs.)"
  208. _len_ru = f" (как минимум {min_len} шт.)"
  209. doc = {
  210. "en": f"series of values{_len_en}{_each_en}, separated with «,»",
  211. "ru": f"списком значений{_len_ru}{_each_ru}, разделенных «,»",
  212. }
  213. super().__init__(
  214. functools.partial(
  215. self._validate,
  216. validator=validator,
  217. min_len=min_len,
  218. max_len=max_len,
  219. fixed_len=fixed_len,
  220. ),
  221. doc,
  222. _internal_id="Series",
  223. )
  224. @staticmethod
  225. def _validate(
  226. value: Any,
  227. /,
  228. *,
  229. validator: Optional[Validator] = None,
  230. min_len: Optional[int] = None,
  231. max_len: Optional[int] = None,
  232. fixed_len: Optional[int] = None,
  233. ):
  234. if not isinstance(value, (list, tuple, set)):
  235. value = str(value).split(",")
  236. if isinstance(value, (tuple, set)):
  237. value = list(value)
  238. if min_len is not None and len(value) < min_len:
  239. raise ValidationError(
  240. f"Passed value ({value}) contains less than {min_len} items"
  241. )
  242. if max_len is not None and len(value) > max_len:
  243. raise ValidationError(
  244. f"Passed value ({value}) contains more than {max_len} items"
  245. )
  246. if fixed_len is not None and len(value) != fixed_len:
  247. raise ValidationError(
  248. f"Passed value ({value}) must contain exactly {fixed_len} items"
  249. )
  250. value = [item.strip() if isinstance(item, str) else item for item in value]
  251. if isinstance(validator, Validator):
  252. for i, item in enumerate(value):
  253. try:
  254. value[i] = validator.validate(item)
  255. except ValidationError:
  256. raise ValidationError(
  257. f"Passed value ({value}) contains invalid item"
  258. f" ({str(item).strip()}), which must be {validator.doc['en']}"
  259. )
  260. value = list(filter(lambda x: x, value))
  261. return value
  262. class Link(Validator):
  263. """Valid url must be specified"""
  264. def __init__(self):
  265. super().__init__(
  266. lambda value: self._validate(value),
  267. {
  268. "en": "link",
  269. "ru": "ссылкой",
  270. },
  271. _internal_id="Link",
  272. )
  273. @staticmethod
  274. def _validate(value: Any, /) -> str:
  275. try:
  276. if not utils.check_url(value):
  277. raise Exception("Invalid URL")
  278. except Exception:
  279. raise ValidationError(f"Passed value ({value}) is not a valid URL")
  280. return value
  281. class String(Validator):
  282. """
  283. Checks for length of passed value and automatically converts it to string
  284. :param length: Exact length of string
  285. """
  286. def __init__(self, length: Optional[int] = None):
  287. if length is not None:
  288. doc = {
  289. "en": f"string of length {length}",
  290. "ru": f"строкой из {length} символа(-ов)",
  291. }
  292. else:
  293. doc = {
  294. "en": "string",
  295. "ru": "строкой",
  296. }
  297. super().__init__(
  298. functools.partial(self._validate, length=length),
  299. doc,
  300. _internal_id="String",
  301. )
  302. @staticmethod
  303. def _validate(value: Any, /, *, length: int) -> str:
  304. if (
  305. isinstance(length, int)
  306. and len(list(grapheme.graphemes(str(value)))) != length
  307. ):
  308. raise ValidationError(
  309. f"Passed value ({value}) must be a length of {length}"
  310. )
  311. return str(value)
  312. class RegExp(Validator):
  313. """
  314. Checks if value matches the regex
  315. :param regex: Regex to match
  316. """
  317. def __init__(self, regex: str):
  318. try:
  319. re.compile(regex)
  320. except re.error as e:
  321. raise Exception(f"{regex} is not a valid regex") from e
  322. doc = {
  323. "en": f"string matching pattern «{regex}»",
  324. "ru": f"строкой, соответствующей шаблону «{regex}»",
  325. }
  326. super().__init__(
  327. functools.partial(self._validate, regex=regex),
  328. doc,
  329. _internal_id="RegExp",
  330. )
  331. @staticmethod
  332. def _validate(value: Any, /, *, regex: str) -> str:
  333. if not re.match(regex, value):
  334. raise ValidationError(f"Passed value ({value}) must follow pattern {regex}")
  335. return value
  336. class Float(Validator):
  337. """
  338. Checks whether passed argument is a float value
  339. :param minimum: Minimal number to be passed
  340. :param maximum: Maximum number to be passed
  341. """
  342. def __init__(
  343. self,
  344. minimum: Optional[float] = None,
  345. maximum: Optional[float] = None,
  346. ):
  347. _sign_en = "positive " if minimum is not None and minimum == 0 else ""
  348. _sign_ru = "положительным " if minimum is not None and minimum == 0 else ""
  349. _sign_en = "negative " if maximum is not None and maximum == 0 else _sign_en
  350. _sign_ru = (
  351. "отрицательным " if maximum is not None and maximum == 0 else _sign_ru
  352. )
  353. if minimum is not None and minimum != 0:
  354. doc = (
  355. {
  356. "en": f"{_sign_en}float greater than {minimum}",
  357. "ru": f"{_sign_ru}дробным числом больше {minimum}",
  358. }
  359. if maximum is None and maximum != 0
  360. else {
  361. "en": f"{_sign_en}float from {minimum} to {maximum}",
  362. "ru": (
  363. f"{_sign_ru}дробным числом в промежутке от {minimum} до"
  364. f" {maximum}"
  365. ),
  366. }
  367. )
  368. elif maximum is None and maximum != 0:
  369. doc = {
  370. "en": f"{_sign_en}float",
  371. "ru": f"{_sign_ru}дробным числом",
  372. }
  373. else:
  374. doc = {
  375. "en": f"{_sign_en}float less than {maximum}",
  376. "ru": f"{_sign_ru}дробным числом меньше {maximum}",
  377. }
  378. super().__init__(
  379. functools.partial(
  380. self._validate,
  381. minimum=minimum,
  382. maximum=maximum,
  383. ),
  384. doc,
  385. _internal_id="Float",
  386. )
  387. @staticmethod
  388. def _validate(
  389. value: Any,
  390. /,
  391. *,
  392. minimum: Optional[float] = None,
  393. maximum: Optional[float] = None,
  394. ) -> TypeUnion[int, None]:
  395. try:
  396. value = float(str(value).strip().replace(",", "."))
  397. except ValueError:
  398. raise ValidationError(f"Passed value ({value}) must be a float")
  399. if minimum is not None and value < minimum:
  400. raise ValidationError(f"Passed value ({value}) is lower than minimum one")
  401. if maximum is not None and value > maximum:
  402. raise ValidationError(f"Passed value ({value}) is greater than maximum one")
  403. return value
  404. class TelegramID(Validator):
  405. def __init__(self):
  406. super().__init__(
  407. self._validate,
  408. "Telegram ID",
  409. _internal_id="TelegramID",
  410. )
  411. @staticmethod
  412. def _validate(value: Any, /):
  413. e = ValidationError(f"Passed value ({value}) is not a valid telegram id")
  414. try:
  415. value = int(str(value).strip())
  416. except Exception:
  417. raise e
  418. if str(value).startswith("-100"):
  419. value = int(str(value)[4:])
  420. if value > 2**32 - 1 or value < 0:
  421. raise e
  422. return value
  423. class Union(Validator):
  424. def __init__(self, *validators):
  425. doc = {
  426. "en": "one of the following:\n",
  427. "ru": "одним из следующего:\n",
  428. }
  429. def case(x: str) -> str:
  430. return x[0].upper() + x[1:]
  431. for validator in validators:
  432. doc["en"] += f"- {case(validator.doc['en'])}\n"
  433. doc["ru"] += f"- {case(validator.doc['ru'])}\n"
  434. doc["en"] = doc["en"].strip()
  435. doc["ru"] = doc["ru"].strip()
  436. super().__init__(
  437. functools.partial(self._validate, validators=validators),
  438. doc,
  439. _internal_id="Union",
  440. )
  441. @staticmethod
  442. def _validate(value: Any, /, *, validators: list):
  443. for validator in validators:
  444. try:
  445. return validator.validate(value)
  446. except ValidationError:
  447. pass
  448. raise ValidationError(f"Passed value ({value}) is not valid")
  449. class NoneType(Validator):
  450. def __init__(self):
  451. super().__init__(
  452. self._validate,
  453. "`None`",
  454. _internal_id="NoneType",
  455. )
  456. @staticmethod
  457. def _validate(value: Any, /) -> None:
  458. if value not in {None, False, ""}:
  459. raise ValidationError(f"Passed value ({value}) is not None")
  460. return None
  461. class Hidden(Validator):
  462. def __init__(self, validator: Optional[Validator] = None):
  463. if not validator:
  464. validator = String()
  465. super().__init__(
  466. functools.partial(self._validate, validator=validator),
  467. validator.doc,
  468. _internal_id="Hidden",
  469. )
  470. @staticmethod
  471. def _validate(value: Any, /, *, validator: Validator) -> Any:
  472. return validator.validate(value)