internal_validation.odin 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. #+private
  2. package flags
  3. @require import "base:runtime"
  4. @require import "core:container/bit_array"
  5. @require import "core:fmt"
  6. @require import "core:mem"
  7. @require import "core:os"
  8. @require import "core:reflect"
  9. @require import "core:strconv"
  10. @require import "core:strings"
  11. // This proc is used to assert that `T` meets the expectations of the library.
  12. @(optimization_mode="favor_size", disabled=ODIN_DISABLE_ASSERT)
  13. validate_structure :: proc(model_type: $T, style: Parsing_Style, loc := #caller_location) {
  14. positionals_assigned_so_far: bit_array.Bit_Array
  15. defer bit_array.destroy(&positionals_assigned_so_far)
  16. check_fields: for field in reflect.struct_fields_zipped(T) {
  17. if style == .Unix {
  18. #partial switch specific_type_info in field.type.variant {
  19. case runtime.Type_Info_Map:
  20. fmt.panicf("%T.%s is a map type, and these are not supported in UNIX-style parsing mode.",
  21. model_type, field.name, loc = loc)
  22. }
  23. }
  24. name_is_safe := true
  25. defer {
  26. fmt.assertf(name_is_safe, "%T.%s is using a reserved name.",
  27. model_type, field.name, loc = loc)
  28. }
  29. switch field.name {
  30. case RESERVED_HELP_FLAG, RESERVED_HELP_FLAG_SHORT:
  31. name_is_safe = false
  32. }
  33. args_tag, ok := reflect.struct_tag_lookup(field.tag, TAG_ARGS)
  34. if !ok {
  35. // If it has no args tag, then we've checked all we need to.
  36. // Most of this proc is validating that the subtags are sane.
  37. continue
  38. }
  39. if name, has_name := get_struct_subtag(args_tag, SUBTAG_NAME); has_name {
  40. fmt.assertf(len(name) > 0, "%T.%s has a zero-length `%s`.",
  41. model_type, field.name, SUBTAG_NAME, loc = loc)
  42. fmt.assertf(strings.index(name, " ") == -1, "%T.%s has a `%s` with spaces in it.",
  43. model_type, field.name, SUBTAG_NAME, loc = loc)
  44. switch name {
  45. case RESERVED_HELP_FLAG, RESERVED_HELP_FLAG_SHORT:
  46. name_is_safe = false
  47. continue check_fields
  48. case:
  49. name_is_safe = true
  50. }
  51. }
  52. pos_str, has_pos := get_struct_subtag(args_tag, SUBTAG_POS)
  53. if has_pos {
  54. #partial switch specific_type_info in field.type.variant {
  55. case runtime.Type_Info_Map:
  56. fmt.panicf("%T.%s has `%s` defined, and this does not make sense on a map type.",
  57. model_type, field.name, SUBTAG_POS, loc = loc)
  58. }
  59. pos_value, pos_ok := strconv.parse_u64_of_base(pos_str, 10)
  60. fmt.assertf(pos_ok, "%T.%s has `%s` defined as %q but cannot be parsed a base-10 integer >= 0.",
  61. model_type, field.name, SUBTAG_POS, pos_str, loc = loc)
  62. fmt.assertf(!bit_array.get(&positionals_assigned_so_far, pos_value), "%T.%s has `%s` set to #%i, but that position has already been assigned to another flag.",
  63. model_type, field.name, SUBTAG_POS, pos_value, loc = loc)
  64. bit_array.set(&positionals_assigned_so_far, pos_value)
  65. }
  66. required_min, required_max: int
  67. if requirement, is_required := get_struct_subtag(args_tag, SUBTAG_REQUIRED); is_required {
  68. fmt.assertf(!reflect.is_boolean(field.type), "%T.%s is a required boolean. This is disallowed.",
  69. model_type, field.name, loc = loc)
  70. fmt.assertf(field.name != INTERNAL_OVERFLOW_FLAG, "%T.%s is defined as required. This is disallowed.",
  71. model_type, field.name, loc = loc)
  72. if len(requirement) > 0 {
  73. if required_min, required_max, ok = parse_requirements(requirement); ok {
  74. #partial switch specific_type_info in field.type.variant {
  75. case runtime.Type_Info_Dynamic_Array:
  76. fmt.assertf(required_min != required_max, "%T.%s has `%s` defined as %q, but the minimum and maximum are the same. Increase the maximum by 1 for an exact number of arguments: (%i<%i)",
  77. model_type,
  78. field.name,
  79. SUBTAG_REQUIRED,
  80. requirement,
  81. required_min,
  82. 1 + required_max,
  83. loc = loc)
  84. fmt.assertf(required_min < required_max, "%T.%s has `%s` defined as %q, but the minimum and maximum are swapped.",
  85. model_type, field.name, SUBTAG_REQUIRED, requirement, loc = loc)
  86. case:
  87. fmt.panicf("%T.%s has `%s` defined as %q, but ranges are only supported on dynamic arrays.",
  88. model_type, field.name, SUBTAG_REQUIRED, requirement, loc = loc)
  89. }
  90. } else {
  91. fmt.panicf("%T.%s has `%s` defined as %q, but it cannot be parsed as a valid range.",
  92. model_type, field.name, SUBTAG_REQUIRED, requirement, loc = loc)
  93. }
  94. }
  95. }
  96. if length, is_manifold := get_struct_subtag(args_tag, SUBTAG_MANIFOLD); is_manifold {
  97. fmt.assertf(!has_pos,
  98. "%T.%s has both `%s` and `%s` defined. This is disallowed.\n\tSuggestion: Use a dynamic array field named `%s` to accept unspecified positional arguments.",
  99. model_type, field.name, SUBTAG_POS, SUBTAG_MANIFOLD, INTERNAL_OVERFLOW_FLAG, loc = loc)
  100. if value, parse_ok := strconv.parse_u64_of_base(length, 10); parse_ok {
  101. fmt.assertf(value > 0,
  102. "%T.%s has `%s` set to %i. It must be greater than zero.",
  103. model_type, field.name, value, SUBTAG_MANIFOLD, loc = loc)
  104. fmt.assertf(value != 1,
  105. "%T.%s has `%s` set to 1. This is equivalent to not defining `%s`.",
  106. model_type, field.name, SUBTAG_MANIFOLD, SUBTAG_MANIFOLD, loc = loc)
  107. }
  108. #partial switch specific_type_info in field.type.variant {
  109. case runtime.Type_Info_Dynamic_Array:
  110. fmt.assertf(style != .Odin,
  111. "%T.%s has `%s` defined, but this only makes sense in UNIX-style parsing mode.",
  112. model_type, field.name, SUBTAG_MANIFOLD, loc = loc)
  113. case:
  114. fmt.panicf("%T.%s has `%s` defined, but this only makes sense on dynamic arrays.",
  115. model_type, field.name, SUBTAG_MANIFOLD, loc = loc)
  116. }
  117. }
  118. allowed_to_define_file_perms: bool = ---
  119. #partial switch specific_type_info in field.type.variant {
  120. case runtime.Type_Info_Map:
  121. allowed_to_define_file_perms = specific_type_info.value.id == os.Handle
  122. case runtime.Type_Info_Dynamic_Array:
  123. allowed_to_define_file_perms = specific_type_info.elem.id == os.Handle
  124. case:
  125. allowed_to_define_file_perms = field.type.id == os.Handle
  126. }
  127. if _, has_file := get_struct_subtag(args_tag, SUBTAG_FILE); has_file {
  128. fmt.assertf(allowed_to_define_file_perms, "%T.%s has `%s` defined, but it is not nor does it contain an `os.Handle` type.",
  129. model_type, field.name, SUBTAG_FILE, loc = loc)
  130. }
  131. if _, has_perms := get_struct_subtag(args_tag, SUBTAG_PERMS); has_perms {
  132. fmt.assertf(allowed_to_define_file_perms, "%T.%s has `%s` defined, but it is not nor does it contain an `os.Handle` type.",
  133. model_type, field.name, SUBTAG_PERMS, loc = loc)
  134. }
  135. #partial switch specific_type_info in field.type.variant {
  136. case runtime.Type_Info_Map:
  137. fmt.assertf(reflect.is_string(specific_type_info.key), "%T.%s is defined as a map[%T]. Only string types are currently supported as map keys.",
  138. model_type,
  139. field.name,
  140. specific_type_info.key)
  141. }
  142. }
  143. }
  144. // Validate that all the required arguments are set and that the set arguments
  145. // are up to the program's expectations.
  146. @(optimization_mode="favor_size")
  147. validate_arguments :: proc(model: ^$T, parser: ^Parser) -> Error {
  148. check_fields: for field, index in reflect.struct_fields_zipped(T) {
  149. was_set := bit_array.get(&parser.fields_set, index)
  150. field_name := get_field_name(field)
  151. args_tag := reflect.struct_tag_get(field.tag, TAG_ARGS)
  152. requirement, is_required := get_struct_subtag(args_tag, SUBTAG_REQUIRED)
  153. required_min, required_max: int
  154. has_requirements: bool
  155. if is_required {
  156. required_min, required_max, has_requirements = parse_requirements(requirement)
  157. }
  158. if has_requirements && required_min == 0 {
  159. // Allow `0<n` or `<n` to bypass the required condition.
  160. is_required = false
  161. }
  162. if _, is_array := field.type.variant.(runtime.Type_Info_Dynamic_Array); is_array && has_requirements {
  163. // If it's an array, make sure it meets the required number of arguments.
  164. ptr := cast(^runtime.Raw_Dynamic_Array)(cast(uintptr)model + field.offset)
  165. if required_min == required_max - 1 && ptr.len != required_min {
  166. return Validation_Error {
  167. fmt.tprintf("The flag `%s` had %i option%s set, but it requires exactly %i.",
  168. field_name,
  169. ptr.len,
  170. "" if ptr.len == 1 else "s",
  171. required_min),
  172. }
  173. } else if required_min > ptr.len || ptr.len >= required_max {
  174. if required_max == max(int) {
  175. return Validation_Error {
  176. fmt.tprintf("The flag `%s` had %i option%s set, but it requires at least %i.",
  177. field_name,
  178. ptr.len,
  179. "" if ptr.len == 1 else "s",
  180. required_min),
  181. }
  182. } else {
  183. return Validation_Error {
  184. fmt.tprintf("The flag `%s` had %i option%s set, but it requires at least %i and at most %i.",
  185. field_name,
  186. ptr.len,
  187. "" if ptr.len == 1 else "s",
  188. required_min,
  189. required_max - 1),
  190. }
  191. }
  192. }
  193. } else if !was_set {
  194. if is_required {
  195. return Validation_Error {
  196. fmt.tprintf("The required flag `%s` was not set.", field_name),
  197. }
  198. }
  199. // Not set, not required; moving on.
  200. continue
  201. }
  202. // All default checks have passed. The program gets a look at it now.
  203. if global_custom_flag_checker != nil {
  204. ptr := cast(rawptr)(cast(uintptr)model + field.offset)
  205. error := global_custom_flag_checker(model,
  206. field.name,
  207. mem.make_any(ptr, field.type.id),
  208. args_tag)
  209. if len(error) > 0 {
  210. // The program reported an error message.
  211. return Validation_Error { error }
  212. }
  213. }
  214. }
  215. return nil
  216. }