habarico 7.2.0.pre.beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +82 -0
  4. data/Rakefile +15 -0
  5. data/VERSION +1 -0
  6. data/app/controllers/habarico/action_schema_controller.rb +41 -0
  7. data/app/controllers/habarico/application_controller.rb +15 -0
  8. data/app/controllers/habarico/contrib/preferences/user_preferences_controller.rb +105 -0
  9. data/app/controllers/habarico/global_schema_controller.rb +11 -0
  10. data/app/controllers/habarico/local_schema_controller.rb +35 -0
  11. data/app/helpers/habarico/application_helper.rb +4 -0
  12. data/app/models/habarico/application_record.rb +5 -0
  13. data/app/views/layouts/habarico/application.html.erb +17 -0
  14. data/config/locales/cs.yml +39 -0
  15. data/config/locales/de.yml +39 -0
  16. data/config/locales/en.yml +39 -0
  17. data/config/locales/es.yml +39 -0
  18. data/config/locales/fr.yml +39 -0
  19. data/config/locales/it.yml +39 -0
  20. data/config/locales/ja.yml +39 -0
  21. data/config/locales/nl.yml +39 -0
  22. data/config/locales/pl.yml +39 -0
  23. data/config/locales/pt.yml +39 -0
  24. data/config/locales/ro.yml +39 -0
  25. data/config/locales/sk.yml +39 -0
  26. data/config/locales/uk.yml +39 -0
  27. data/config/routes.rb +1 -0
  28. data/lib/generators/habarico/preferences/install_generator.rb +27 -0
  29. data/lib/generators/habarico/preferences/templates/create_habarico_user_preferences.rb.tt +15 -0
  30. data/lib/habarico/active_model/references.rb +68 -0
  31. data/lib/habarico/authenticatable.rb +31 -0
  32. data/lib/habarico/config.rb +71 -0
  33. data/lib/habarico/context.rb +126 -0
  34. data/lib/habarico/contrib/preferences/base_preferences.rb +40 -0
  35. data/lib/habarico/contrib/preferences/config.rb +31 -0
  36. data/lib/habarico/contrib/preferences/errors.rb +79 -0
  37. data/lib/habarico/contrib/preferences/locales/cs.yml +8 -0
  38. data/lib/habarico/contrib/preferences/locales/de.yml +8 -0
  39. data/lib/habarico/contrib/preferences/locales/en.yml +8 -0
  40. data/lib/habarico/contrib/preferences/locales/es.yml +8 -0
  41. data/lib/habarico/contrib/preferences/locales/fr.yml +8 -0
  42. data/lib/habarico/contrib/preferences/locales/it.yml +8 -0
  43. data/lib/habarico/contrib/preferences/locales/ja.yml +8 -0
  44. data/lib/habarico/contrib/preferences/locales/nl.yml +8 -0
  45. data/lib/habarico/contrib/preferences/locales/pl.yml +8 -0
  46. data/lib/habarico/contrib/preferences/locales/pt.yml +8 -0
  47. data/lib/habarico/contrib/preferences/locales/ro.yml +8 -0
  48. data/lib/habarico/contrib/preferences/locales/sk.yml +8 -0
  49. data/lib/habarico/contrib/preferences/locales/uk.yml +8 -0
  50. data/lib/habarico/contrib/preferences/registry.rb +293 -0
  51. data/lib/habarico/contrib/preferences/routes.rb +17 -0
  52. data/lib/habarico/contrib/preferences/schemas.rb +129 -0
  53. data/lib/habarico/contrib/preferences/serializer.rb +19 -0
  54. data/lib/habarico/contrib/preferences/user_preference.rb +16 -0
  55. data/lib/habarico/contrib/preferences.rb +32 -0
  56. data/lib/habarico/controller.rb +584 -0
  57. data/lib/habarico/definition/action_reference.rb +18 -0
  58. data/lib/habarico/definition/actions/action.rb +14 -0
  59. data/lib/habarico/definition/actions/create_action.rb +18 -0
  60. data/lib/habarico/definition/actions/delete_action.rb +20 -0
  61. data/lib/habarico/definition/actions/edit_action.rb +18 -0
  62. data/lib/habarico/definition/actions/link_action.rb +11 -0
  63. data/lib/habarico/definition/actions/resource_action.rb +12 -0
  64. data/lib/habarico/definition/actions/save_action.rb +14 -0
  65. data/lib/habarico/definition/actions/show_action.rb +18 -0
  66. data/lib/habarico/definition/actions/upload_images_action.rb +11 -0
  67. data/lib/habarico/definition/actions/view_action.rb +11 -0
  68. data/lib/habarico/definition/boxes/array_box.rb +10 -0
  69. data/lib/habarico/definition/boxes/box.rb +32 -0
  70. data/lib/habarico/definition/boxes/gallery_box.rb +10 -0
  71. data/lib/habarico/definition/boxes/grid_box.rb +10 -0
  72. data/lib/habarico/definition/boxes/grid_item_box.rb +10 -0
  73. data/lib/habarico/definition/boxes/list_box.rb +10 -0
  74. data/lib/habarico/definition/boxes/object_box.rb +10 -0
  75. data/lib/habarico/definition/boxes/tab_box.rb +10 -0
  76. data/lib/habarico/definition/boxes/tab_item_box.rb +10 -0
  77. data/lib/habarico/definition/boxes/wizard_box.rb +10 -0
  78. data/lib/habarico/definition/concerns/definition_model.rb +38 -0
  79. data/lib/habarico/definition/definition.rb +206 -0
  80. data/lib/habarico/definition/field_reference.rb +9 -0
  81. data/lib/habarico/definition/filter_reference.rb +7 -0
  82. data/lib/habarico/definition/menu/link_menu_item.rb +12 -0
  83. data/lib/habarico/definition/menu/menu_item.rb +9 -0
  84. data/lib/habarico/definition/menu/text_menu_item.rb +12 -0
  85. data/lib/habarico/definition/menu/view_menu_item.rb +13 -0
  86. data/lib/habarico/definition/response.rb +13 -0
  87. data/lib/habarico/definition/views/action_view.rb +11 -0
  88. data/lib/habarico/definition/views/create_view.rb +15 -0
  89. data/lib/habarico/definition/views/edit_view.rb +15 -0
  90. data/lib/habarico/definition/views/list_view.rb +17 -0
  91. data/lib/habarico/definition/views/show_view.rb +15 -0
  92. data/lib/habarico/definition/views/view.rb +10 -0
  93. data/lib/habarico/engine.rb +6 -0
  94. data/lib/habarico/file_validations.rb +119 -0
  95. data/lib/habarico/generators/abstract_generator.rb +21 -0
  96. data/lib/habarico/generators/action/action_schema_generator.rb +111 -0
  97. data/lib/habarico/generators/action/confirm_dialog_properties_generator.rb +30 -0
  98. data/lib/habarico/generators/action/global_resource_action_schema_generator.rb +46 -0
  99. data/lib/habarico/generators/action/link_action_parameters_generator.rb +31 -0
  100. data/lib/habarico/generators/action/resource_action_endpoint_parameters_generator.rb +70 -0
  101. data/lib/habarico/generators/action/resource_action_parameters_generator.rb +40 -0
  102. data/lib/habarico/generators/action/resource_action_schema_generator.rb +51 -0
  103. data/lib/habarico/generators/action/save_action_parameters_generator.rb +26 -0
  104. data/lib/habarico/generators/action/upload_images_action_parameters_generator.rb +41 -0
  105. data/lib/habarico/generators/action/view_action_endpoint_parameters_generator.rb +37 -0
  106. data/lib/habarico/generators/action/view_action_parameters_generator.rb +56 -0
  107. data/lib/habarico/generators/endpoint/dynamic_render_options_generator.rb +26 -0
  108. data/lib/habarico/generators/endpoint/endpoints_schema_generator.rb +61 -0
  109. data/lib/habarico/generators/endpoint/enum_type_generator.rb +45 -0
  110. data/lib/habarico/generators/endpoint/field_options_generator.rb +179 -0
  111. data/lib/habarico/generators/endpoint/field_schema_generator.rb +381 -0
  112. data/lib/habarico/generators/endpoint/fields_schema_generator.rb +35 -0
  113. data/lib/habarico/generators/endpoint/filter_schema_generator.rb +201 -0
  114. data/lib/habarico/generators/endpoint/filters_schema_generator.rb +25 -0
  115. data/lib/habarico/generators/endpoint/method_schema_generator.rb +34 -0
  116. data/lib/habarico/generators/endpoint/number_field_options_generator.rb +45 -0
  117. data/lib/habarico/generators/endpoint/phone_field_options_generator.rb +29 -0
  118. data/lib/habarico/generators/endpoint/relation_field_options_generator.rb +90 -0
  119. data/lib/habarico/generators/endpoint/search_filter_schema_generator.rb +44 -0
  120. data/lib/habarico/generators/menu/menu_group_schema_generator.rb +63 -0
  121. data/lib/habarico/generators/menu/menu_link_parameters_generator.rb +31 -0
  122. data/lib/habarico/generators/menu/menu_schema_generator.rb +105 -0
  123. data/lib/habarico/generators/menu/menu_view_endpoint_parameters_generator.rb +44 -0
  124. data/lib/habarico/generators/menu/menu_view_parameters_generator.rb +50 -0
  125. data/lib/habarico/generators/resource/action_resource_schema_generator.rb +84 -0
  126. data/lib/habarico/generators/resource/global_resource_schema_generator.rb +70 -0
  127. data/lib/habarico/generators/resource/local_resource_schema_generator.rb +111 -0
  128. data/lib/habarico/generators/resource/object_resource_schema_generator.rb +49 -0
  129. data/lib/habarico/generators/resource/resource_schema_generator.rb +31 -0
  130. data/lib/habarico/generators/view/action_descriptor_generator.rb +88 -0
  131. data/lib/habarico/generators/view/array_box_parameters_generator.rb +34 -0
  132. data/lib/habarico/generators/view/box_options_generator.rb +23 -0
  133. data/lib/habarico/generators/view/box_schema_generator.rb +194 -0
  134. data/lib/habarico/generators/view/detail_view_parameters_generator.rb +57 -0
  135. data/lib/habarico/generators/view/field_descriptor_generator.rb +107 -0
  136. data/lib/habarico/generators/view/filter_descriptor_generator.rb +75 -0
  137. data/lib/habarico/generators/view/gallery_box_parameters_generator.rb +71 -0
  138. data/lib/habarico/generators/view/global_view_schema_generator.rb +59 -0
  139. data/lib/habarico/generators/view/grid_item_box_parameters_generator.rb +44 -0
  140. data/lib/habarico/generators/view/list_box_parameters_generator.rb +124 -0
  141. data/lib/habarico/generators/view/list_view_parameters_generator.rb +120 -0
  142. data/lib/habarico/generators/view/object_box_parameters_generator.rb +26 -0
  143. data/lib/habarico/generators/view/sort_descriptor_generator.rb +30 -0
  144. data/lib/habarico/generators/view/tab_box_parameters_generator.rb +26 -0
  145. data/lib/habarico/generators/view/view_endpoint_parameters_generator.rb +71 -0
  146. data/lib/habarico/generators/view/view_schema_generator.rb +87 -0
  147. data/lib/habarico/generators/view/wizard_box_parameters_generator.rb +32 -0
  148. data/lib/habarico/helpers/action_helper.rb +9 -0
  149. data/lib/habarico/helpers/attributes_helper.rb +67 -0
  150. data/lib/habarico/helpers/box_helper.rb +19 -0
  151. data/lib/habarico/helpers/callable_helper.rb +15 -0
  152. data/lib/habarico/helpers/filter_params_helper.rb +49 -0
  153. data/lib/habarico/helpers/order_by_helper.rb +56 -0
  154. data/lib/habarico/helpers/reflection_helper.rb +61 -0
  155. data/lib/habarico/helpers/sorting_helper.rb +136 -0
  156. data/lib/habarico/schema/action/action_schema.rb +46 -0
  157. data/lib/habarico/schema/action/confirm_dialog_properties.rb +13 -0
  158. data/lib/habarico/schema/action/global_resource_action_schema.rb +29 -0
  159. data/lib/habarico/schema/action/link_action_parameters.rb +15 -0
  160. data/lib/habarico/schema/action/resource_action_endpoint_parameters.rb +24 -0
  161. data/lib/habarico/schema/action/resource_action_parameters.rb +17 -0
  162. data/lib/habarico/schema/action/resource_action_schema.rb +14 -0
  163. data/lib/habarico/schema/action/save_action_parameters.rb +10 -0
  164. data/lib/habarico/schema/action/upload_images_action_parameters.rb +23 -0
  165. data/lib/habarico/schema/action/view_action_endpoint_parameters.rb +16 -0
  166. data/lib/habarico/schema/action/view_action_parameters.rb +27 -0
  167. data/lib/habarico/schema/endpoint/ai_field_options.rb +10 -0
  168. data/lib/habarico/schema/endpoint/box_options.rb +10 -0
  169. data/lib/habarico/schema/endpoint/choices_field_options.rb +10 -0
  170. data/lib/habarico/schema/endpoint/dynamic_render_options.rb +14 -0
  171. data/lib/habarico/schema/endpoint/endpoints_schema.rb +22 -0
  172. data/lib/habarico/schema/endpoint/enum_type.rb +21 -0
  173. data/lib/habarico/schema/endpoint/field_options.rb +56 -0
  174. data/lib/habarico/schema/endpoint/field_schema.rb +65 -0
  175. data/lib/habarico/schema/endpoint/filter_schema.rb +44 -0
  176. data/lib/habarico/schema/endpoint/image_field_options.rb +13 -0
  177. data/lib/habarico/schema/endpoint/key_value.rb +13 -0
  178. data/lib/habarico/schema/endpoint/method_schema.rb +13 -0
  179. data/lib/habarico/schema/endpoint/number_field_options.rb +24 -0
  180. data/lib/habarico/schema/endpoint/phone_field_options.rb +13 -0
  181. data/lib/habarico/schema/endpoint/relation_field_options.rb +39 -0
  182. data/lib/habarico/schema/extensible_enum.rb +20 -0
  183. data/lib/habarico/schema/menu/action_view_choice.rb +13 -0
  184. data/lib/habarico/schema/menu/menu_group_schema.rb +26 -0
  185. data/lib/habarico/schema/menu/menu_schema.rb +36 -0
  186. data/lib/habarico/schema/resource/global_resource_schema.rb +14 -0
  187. data/lib/habarico/schema/resource/local_resource_schema.rb +14 -0
  188. data/lib/habarico/schema/resource/object_resource_schema.rb +8 -0
  189. data/lib/habarico/schema/resource/resource_schema.rb +16 -0
  190. data/lib/habarico/schema/types.rb +102 -0
  191. data/lib/habarico/schema/view/action_descriptor.rb +30 -0
  192. data/lib/habarico/schema/view/array_box_parameters.rb +13 -0
  193. data/lib/habarico/schema/view/box_schema.rb +67 -0
  194. data/lib/habarico/schema/view/detail_view_parameters.rb +16 -0
  195. data/lib/habarico/schema/view/field_descriptor.rb +50 -0
  196. data/lib/habarico/schema/view/field_wrapper_properties.rb +13 -0
  197. data/lib/habarico/schema/view/filter_descriptor.rb +24 -0
  198. data/lib/habarico/schema/view/gallery_box_parameters.rb +31 -0
  199. data/lib/habarico/schema/view/global_view_schema.rb +25 -0
  200. data/lib/habarico/schema/view/grid_item_box_parameters.rb +13 -0
  201. data/lib/habarico/schema/view/list_box_parameters.rb +44 -0
  202. data/lib/habarico/schema/view/list_view_parameters.rb +31 -0
  203. data/lib/habarico/schema/view/object_box_parameters.rb +10 -0
  204. data/lib/habarico/schema/view/sort_descriptor.rb +15 -0
  205. data/lib/habarico/schema/view/sortable_parameters.rb +13 -0
  206. data/lib/habarico/schema/view/tab_box_parameters.rb +10 -0
  207. data/lib/habarico/schema/view/view_action_descriptor_parameters.rb +21 -0
  208. data/lib/habarico/schema/view/view_endpoint_parameters.rb +22 -0
  209. data/lib/habarico/schema/view/view_schema.rb +33 -0
  210. data/lib/habarico/schema/view/wizard_box_parameters.rb +13 -0
  211. data/lib/habarico/search/field_resolver.rb +138 -0
  212. data/lib/habarico/search/search_matcher.rb +152 -0
  213. data/lib/habarico/serializers/meta_serializer.rb +42 -0
  214. data/lib/habarico/serializers/record_serializer.rb +33 -0
  215. data/lib/habarico/serializers/serializer.rb +61 -0
  216. data/lib/habarico/services/resource_service.rb +69 -0
  217. data/lib/habarico/services/routing_service.rb +160 -0
  218. data/lib/habarico/services/schema_service.rb +72 -0
  219. data/lib/habarico/version.rb +5 -0
  220. data/lib/habarico.rb +262 -0
  221. metadata +324 -0
@@ -0,0 +1,79 @@
1
+ module Habarico
2
+ module Contrib
3
+ module Preferences
4
+ # Base class for all preference errors.
5
+ class PreferenceError < StandardError; end
6
+
7
+ # Raised when a key is neither registered exactly nor matched by a pattern.
8
+ class PreferenceNotRegistered < PreferenceError
9
+ attr_reader :key
10
+
11
+ def initialize(key)
12
+ @key = key
13
+ super(%(Preference key "#{key}" is not registered.))
14
+ end
15
+ end
16
+
17
+ # Raised when the same exact key is registered twice.
18
+ class PreferenceAlreadyRegistered < PreferenceError
19
+ attr_reader :key
20
+
21
+ def initialize(key)
22
+ @key = key
23
+ super(%(Preference key "#{key}" is already registered.))
24
+ end
25
+ end
26
+
27
+ # Raised when a key does not match the allowed key format.
28
+ class InvalidPreferenceKey < PreferenceError
29
+ attr_reader :key
30
+
31
+ def initialize(key)
32
+ @key = key
33
+ super(
34
+ %(Preference key "#{key}" is invalid. Keys must match ) +
35
+ %("^[a-zA-Z][a-zA-Z0-9_./-]*$" (ASCII letters, digits, underscore, dot, ) +
36
+ %(slash, hyphen; must start with a letter).)
37
+ )
38
+ end
39
+ end
40
+
41
+ # Base for the size and count guards. Both surface as 422, like a schema mismatch.
42
+ class PreferenceLimitExceeded < PreferenceError; end
43
+
44
+ # Raised when a value serializes to more bytes than the key's limit allows.
45
+ class PreferenceValueTooLarge < PreferenceLimitExceeded
46
+ attr_reader :key, :limit, :actual
47
+
48
+ def initialize(key, limit, actual)
49
+ @key = key
50
+ @limit = limit
51
+ @actual = actual
52
+ super(%(Value for preference "#{key}" is #{actual} bytes, over the #{limit} byte limit.))
53
+ end
54
+ end
55
+
56
+ # Raised when storing a new key would take a user past `max_keys_per_user`.
57
+ class TooManyPreferences < PreferenceLimitExceeded
58
+ attr_reader :key, :limit
59
+
60
+ def initialize(key, limit)
61
+ @key = key
62
+ @limit = limit
63
+ super(%(Cannot store preference "#{key}": the limit of #{limit} preferences per user is already reached.))
64
+ end
65
+ end
66
+
67
+ # Raised when a value fails JSON Schema validation for its key.
68
+ class PreferenceValidationError < PreferenceError
69
+ attr_reader :key, :validation_message
70
+
71
+ def initialize(key, message)
72
+ @key = key
73
+ @validation_message = message
74
+ super(%(Value for preference "#{key}" failed validation: #{message}))
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,8 @@
1
+ cs:
2
+ habarico:
3
+ contrib:
4
+ preferences:
5
+ not_found: "Předvolba nenalezena."
6
+ invalid_value: "Neplatná hodnota předvolby."
7
+ value_too_large: "Hodnota předvolby je příliš velká."
8
+ too_many_preferences: "Pro tohoto uživatele je uloženo příliš mnoho předvoleb."
@@ -0,0 +1,8 @@
1
+ de:
2
+ habarico:
3
+ contrib:
4
+ preferences:
5
+ not_found: "Einstellung nicht gefunden."
6
+ invalid_value: "Ungültiger Wert für die Einstellung."
7
+ value_too_large: "Der Wert der Einstellung ist zu groß."
8
+ too_many_preferences: "Für diesen Benutzer sind zu viele Einstellungen gespeichert."
@@ -0,0 +1,8 @@
1
+ en:
2
+ habarico:
3
+ contrib:
4
+ preferences:
5
+ not_found: "Preference not found."
6
+ invalid_value: "Invalid value for preference."
7
+ value_too_large: "Preference value is too large."
8
+ too_many_preferences: "Too many preferences are stored for this user."
@@ -0,0 +1,8 @@
1
+ es:
2
+ habarico:
3
+ contrib:
4
+ preferences:
5
+ not_found: "Preferencia no encontrada."
6
+ invalid_value: "Valor no válido para la preferencia."
7
+ value_too_large: "El valor de la preferencia es demasiado grande."
8
+ too_many_preferences: "Hay demasiadas preferencias almacenadas para este usuario."
@@ -0,0 +1,8 @@
1
+ fr:
2
+ habarico:
3
+ contrib:
4
+ preferences:
5
+ not_found: "Préférence introuvable."
6
+ invalid_value: "Valeur non valide pour la préférence."
7
+ value_too_large: "La valeur de la préférence est trop volumineuse."
8
+ too_many_preferences: "Trop de préférences sont enregistrées pour cet utilisateur."
@@ -0,0 +1,8 @@
1
+ it:
2
+ habarico:
3
+ contrib:
4
+ preferences:
5
+ not_found: "Preferenza non trovata."
6
+ invalid_value: "Valore non valido per la preferenza."
7
+ value_too_large: "Il valore della preferenza è troppo grande."
8
+ too_many_preferences: "Sono memorizzate troppe preferenze per questo utente."
@@ -0,0 +1,8 @@
1
+ ja:
2
+ habarico:
3
+ contrib:
4
+ preferences:
5
+ not_found: "設定が見つかりません。"
6
+ invalid_value: "設定の値が無効です。"
7
+ value_too_large: "設定の値が大きすぎます。"
8
+ too_many_preferences: "このユーザーに保存されている設定が多すぎます。"
@@ -0,0 +1,8 @@
1
+ nl:
2
+ habarico:
3
+ contrib:
4
+ preferences:
5
+ not_found: "Voorkeur niet gevonden."
6
+ invalid_value: "Ongeldige waarde voor voorkeur."
7
+ value_too_large: "De waarde van de voorkeur is te groot."
8
+ too_many_preferences: "Er zijn te veel voorkeuren opgeslagen voor deze gebruiker."
@@ -0,0 +1,8 @@
1
+ pl:
2
+ habarico:
3
+ contrib:
4
+ preferences:
5
+ not_found: "Nie znaleziono preferencji."
6
+ invalid_value: "Nieprawidłowa wartość preferencji."
7
+ value_too_large: "Wartość preferencji jest zbyt duża."
8
+ too_many_preferences: "Dla tego użytkownika zapisano zbyt wiele preferencji."
@@ -0,0 +1,8 @@
1
+ pt:
2
+ habarico:
3
+ contrib:
4
+ preferences:
5
+ not_found: "Preferência não encontrada."
6
+ invalid_value: "Valor inválido para a preferência."
7
+ value_too_large: "O valor da preferência é demasiado grande."
8
+ too_many_preferences: "Estão armazenadas demasiadas preferências para este utilizador."
@@ -0,0 +1,8 @@
1
+ ro:
2
+ habarico:
3
+ contrib:
4
+ preferences:
5
+ not_found: "Preferința nu a fost găsită."
6
+ invalid_value: "Valoare invalidă pentru preferință."
7
+ value_too_large: "Valoarea preferinței este prea mare."
8
+ too_many_preferences: "Sunt stocate prea multe preferințe pentru acest utilizator."
@@ -0,0 +1,8 @@
1
+ sk:
2
+ habarico:
3
+ contrib:
4
+ preferences:
5
+ not_found: "Predvoľba sa nenašla."
6
+ invalid_value: "Neplatná hodnota predvoľby."
7
+ value_too_large: "Hodnota predvoľby je príliš veľká."
8
+ too_many_preferences: "Pre tohto používateľa je uložených príliš veľa predvolieb."
@@ -0,0 +1,8 @@
1
+ uk:
2
+ habarico:
3
+ contrib:
4
+ preferences:
5
+ not_found: "Налаштування не знайдено."
6
+ invalid_value: "Недійсне значення налаштування."
7
+ value_too_large: "Значення налаштування завелике."
8
+ too_many_preferences: "Для цього користувача збережено забагато налаштувань."
@@ -0,0 +1,293 @@
1
+ require "json"
2
+ require "json_schemer"
3
+ require "securerandom"
4
+ require "habarico/contrib/preferences/errors"
5
+
6
+ module Habarico
7
+ module Contrib
8
+ module Preferences
9
+ # Regex a preference key must match. Mirrors the Python KEY_PATTERN.
10
+ KEY_PATTERN = %r{\A[a-zA-Z][a-zA-Z0-9_./-]*\z}
11
+
12
+ # Sentinel meaning "no default registered". Distinct from nil, which is a valid
13
+ # stored/default value.
14
+ EMPTY = Object.new
15
+ def EMPTY.inspect = "EMPTY"
16
+
17
+ # Value object returned by get_value/get_values/set_value.
18
+ ResolvedPreference = Struct.new(:key, :value, :updated_at, keyword_init: true)
19
+
20
+ # One registered entry: its JSON Schema, its (possibly EMPTY/callable) default, a compiled
21
+ # json_schemer validator and its already-resolved size limit (nil meaning unlimited).
22
+ #
23
+ # `max_bytes` needs no sentinel, unlike `default`: nil is not a usable size, so it simply
24
+ # means "not given at registration" and defers to `max_value_bytes`. It is resolved once, at
25
+ # registration, exactly like `defaults` - nothing reads the configuration per write.
26
+ Entry = Struct.new(:schema, :default, :validator, :max_bytes, keyword_init: true)
27
+
28
+ class PreferenceRegistry
29
+ def initialize
30
+ @entries = {} # key (String) => Entry
31
+ @pattern_entries = [] # [[Regexp, Entry], ...]
32
+ end
33
+
34
+ # Test/reload hook.
35
+ def _clear
36
+ @entries.clear
37
+ @pattern_entries.clear
38
+ end
39
+
40
+ def register(key, schema:, default: EMPTY, max_bytes: nil)
41
+ raise InvalidPreferenceKey, key unless key.is_a?(String) && key.match?(KEY_PATTERN)
42
+ raise PreferenceAlreadyRegistered, key if @entries.key?(key)
43
+
44
+ @entries[key] = build_entry(key, schema, default, max_bytes)
45
+ nil
46
+ end
47
+
48
+ def register_pattern(pattern, schema:, max_bytes: nil)
49
+ # A pattern entry never carries a default - there is no single key to attach one to - so
50
+ # `build_entry` returns on its EMPTY branch without validating anything. Same entry, one
51
+ # place: the two registration paths cannot drift when a field is added to `Entry`.
52
+ compiled = pattern.is_a?(Regexp) ? pattern : Regexp.new(pattern)
53
+ @pattern_entries << [compiled, build_entry(compiled.source, schema, EMPTY, max_bytes)]
54
+ nil
55
+ end
56
+
57
+ def registered?(key)
58
+ return true if @entries.key?(key)
59
+
60
+ @pattern_entries.any? { |pattern, _| full_match?(pattern, key) }
61
+ end
62
+
63
+ def get_schema(key)
64
+ entry_for(key).schema
65
+ end
66
+
67
+ def get_default(key)
68
+ default = resolve_default(entry_for(key))
69
+ default.equal?(EMPTY) ? nil : default
70
+ end
71
+
72
+ # Size limit in bytes for a key, or nil when it is unlimited.
73
+ def max_bytes_for(key)
74
+ entry_for(key).max_bytes
75
+ end
76
+
77
+ def validate(key, value)
78
+ entry = entry_for(key)
79
+ ensure_size_allowed!(key, entry, value)
80
+ errors = entry.validator.validate(value).to_a
81
+ raise PreferenceValidationError.new(key, errors.first&.dig("error").to_s) unless errors.empty?
82
+
83
+ value
84
+ end
85
+
86
+ def get_value(user_id, key)
87
+ entry = entry_for(key)
88
+ record = UserPreference.find_by(user_id: user_id, key: key)
89
+ resolve(key, entry, record)
90
+ end
91
+
92
+ def get_values(user_id)
93
+ resolved = {}
94
+
95
+ UserPreference.where(user_id: user_id).find_each do |record|
96
+ entry = begin
97
+ entry_for(record.key)
98
+ rescue PreferenceNotRegistered
99
+ record.destroy
100
+ next
101
+ end
102
+ item = resolve(record.key, entry, record)
103
+ resolved[record.key] = item if item
104
+ end
105
+
106
+ @entries.each do |key, entry|
107
+ next if resolved.key?(key)
108
+
109
+ item = resolve(key, entry, nil)
110
+ resolved[key] = item if item
111
+ end
112
+
113
+ resolved.keys.sort.map { |key| resolved[key] }
114
+ end
115
+
116
+ def set_value(user_id, key, value)
117
+ validate(key, value)
118
+ ensure_key_budget!(user_id, key)
119
+ record = UserPreference.find_or_initialize_by(user_id: user_id, key: key)
120
+ record.id ||= SecureRandom.uuid
121
+ record.value = value
122
+ record.updated_at = Time.current
123
+ record.save!
124
+ ResolvedPreference.new(key: key, value: record.value, updated_at: record.updated_at)
125
+ end
126
+
127
+ def delete_value(user_id, key)
128
+ entry_for(key) # raises PreferenceNotRegistered for unknown keys
129
+ UserPreference.where(user_id: user_id, key: key).delete_all
130
+ nil
131
+ end
132
+
133
+ def delete_values(user_id)
134
+ UserPreference.where(user_id: user_id).delete_all
135
+ nil
136
+ end
137
+
138
+ private
139
+
140
+ # Ahead of the schema: the size guard is the cheap check and the one that has to hold for
141
+ # free-form branches (a saved query's `filter`) the schema does not bound at all.
142
+ def ensure_size_allowed!(key, entry, value)
143
+ limit = entry.max_bytes
144
+ return if limit.nil?
145
+
146
+ size = JSON.generate(value).bytesize
147
+ raise PreferenceValueTooLarge.new(key, limit, size) if size > limit
148
+ end
149
+
150
+ # Configured limit for the key - or pattern - a preference is being registered under. An
151
+ # explicit `max_bytes:` wins; otherwise `max_value_bytes` decides, by the very string the
152
+ # preference is registered under, falling back to its "default" entry; a nil there means
153
+ # unlimited. A pattern may be configured as a Regexp or as its source string. Read once, at
154
+ # registration, exactly like `defaults`: changing the config afterwards does not move an
155
+ # already-registered limit.
156
+ def resolve_max_bytes(registered_as, max_bytes)
157
+ return max_bytes unless max_bytes.nil?
158
+
159
+ limits = Preferences.max_value_bytes || {}
160
+ limits.each do |configured, bytes|
161
+ name = configured.is_a?(Regexp) ? configured.source : configured.to_s
162
+ return bytes if name == registered_as
163
+ end
164
+ limits["default"]
165
+ end
166
+
167
+ # Refuses a NEW key once the user holds the maximum number of preferences. Overwriting a
168
+ # key they already have is always allowed, so nobody already past the limit is locked out
169
+ # of shrinking or fixing what they stored.
170
+ def ensure_key_budget!(user_id, key)
171
+ limit = Preferences.max_keys_per_user
172
+ return if limit.nil?
173
+
174
+ every_key_of_user = UserPreference.where(user_id: user_id)
175
+ # Overwriting a key the user already has adds no row, so it never spends budget - and
176
+ # without this guard a user sitting exactly at the limit could not rewrite anything.
177
+ return if every_key_of_user.where(key: key).exists?
178
+ raise TooManyPreferences.new(key, limit) if every_key_of_user.count >= limit
179
+ end
180
+
181
+ # json_schemer patterns must match the WHOLE key, like Python's fullmatch.
182
+ def full_match?(pattern, key)
183
+ match = pattern.match(key)
184
+ !match.nil? && match[0] == key
185
+ end
186
+
187
+ def entry_for(key)
188
+ return @entries[key] if @entries.key?(key)
189
+
190
+ @pattern_entries.each { |pattern, entry| return entry if full_match?(pattern, key) }
191
+ raise PreferenceNotRegistered, key
192
+ end
193
+
194
+ def build_entry(key, schema, default, max_bytes = nil)
195
+ validator = JSONSchemer.schema(schema)
196
+ entry = Entry.new(schema: schema, default: default, validator: validator,
197
+ max_bytes: resolve_max_bytes(key, max_bytes))
198
+ return entry if default.equal?(EMPTY) || default.respond_to?(:call)
199
+
200
+ errors = validator.validate(default).to_a
201
+ raise PreferenceValidationError.new(key, errors.first&.dig("error").to_s) unless errors.empty?
202
+
203
+ entry
204
+ end
205
+
206
+ # Resolves a (possibly callable) default. A callable that returns EMPTY or an
207
+ # invalid value degrades to EMPTY, mirroring Python `_resolve_default`.
208
+ def resolve_default(entry)
209
+ default = entry.default
210
+ return default unless default.respond_to?(:call)
211
+
212
+ resolved = default.call
213
+ return EMPTY if resolved.equal?(EMPTY)
214
+ return EMPTY unless entry.validator.valid?(resolved)
215
+
216
+ resolved
217
+ end
218
+
219
+ # Mirrors Python `_resolve`: a stored value that fails validation is deleted and the
220
+ # default is returned instead; when there is neither a valid stored value nor a default,
221
+ # returns nil (single-GET => 404, list => omit).
222
+ def resolve(key, entry, record)
223
+ if record
224
+ if entry.validator.valid?(record.value)
225
+ return ResolvedPreference.new(key: key, value: record.value, updated_at: record.updated_at)
226
+ end
227
+
228
+ record.destroy
229
+ end
230
+
231
+ default = resolve_default(entry)
232
+ return nil if default.equal?(EMPTY)
233
+
234
+ ResolvedPreference.new(key: key, value: default, updated_at: nil)
235
+ end
236
+ end
237
+
238
+ # ---- module-level singleton API (mirrors Python module functions) ----
239
+
240
+ def self.registry
241
+ @registry ||= PreferenceRegistry.new
242
+ end
243
+
244
+ def self.register_preference(key, schema:, default: EMPTY, max_bytes: nil)
245
+ registry.register(key, schema: schema, default: default, max_bytes: max_bytes)
246
+ end
247
+
248
+ def self.register_preference_pattern(pattern, schema:, max_bytes: nil)
249
+ registry.register_pattern(pattern, schema: schema, max_bytes: max_bytes)
250
+ end
251
+
252
+ def self.max_bytes_for(key)
253
+ registry.max_bytes_for(key)
254
+ end
255
+
256
+ def self.registered?(key)
257
+ registry.registered?(key)
258
+ end
259
+
260
+ def self.get_schema(key)
261
+ registry.get_schema(key)
262
+ end
263
+
264
+ def self.get_default(key)
265
+ registry.get_default(key)
266
+ end
267
+
268
+ def self.validate(key, value)
269
+ registry.validate(key, value)
270
+ end
271
+
272
+ def self.get_value(user_id, key)
273
+ registry.get_value(user_id, key)
274
+ end
275
+
276
+ def self.get_values(user_id)
277
+ registry.get_values(user_id)
278
+ end
279
+
280
+ def self.set_value(user_id, key, value)
281
+ registry.set_value(user_id, key, value)
282
+ end
283
+
284
+ def self.delete_value(user_id, key)
285
+ registry.delete_value(user_id, key)
286
+ end
287
+
288
+ def self.delete_values(user_id)
289
+ registry.delete_values(user_id)
290
+ end
291
+ end
292
+ end
293
+ end
@@ -0,0 +1,17 @@
1
+ # Registers the user-preferences routes through the RoutingService extension hook, so the
2
+ # host's config/routes.rb (a single `RoutingService.instance.draw_routes`) needs no change.
3
+ # The blocks are drawn at the admin-site root (beside schema/search) - matching the Python
4
+ # AdminSite, where user-preferences lives next to schema/search. RoutingService draws them
5
+ # inside a path-only `scope` (never a `namespace`/`scope module:`), so the controller path
6
+ # must NOT start with a slash: Rails only strips a leading slash when a module scope is
7
+ # active, and raises "is not a supported controller name" otherwise.
8
+ # Keys may contain dots and slashes, so the `:key` segment is a glob with format off.
9
+ Habarico::RoutingService.register_route_block do
10
+ controller_path = "habarico/contrib/preferences/user_preferences"
11
+
12
+ get "user-preferences", to: "#{controller_path}#index"
13
+ delete "user-preferences", to: "#{controller_path}#destroy_all"
14
+ get "user-preferences/*key", to: "#{controller_path}#show", format: false
15
+ put "user-preferences/*key", to: "#{controller_path}#update", format: false
16
+ delete "user-preferences/*key", to: "#{controller_path}#destroy", format: false
17
+ end
@@ -0,0 +1,129 @@
1
+ module Habarico
2
+ module Contrib
3
+ module Preferences
4
+ # Built-in preference value schemas, mirroring the pydantic models in the Python
5
+ # habarico/contrib/preferences/schemas.py. Plain JSON Schema (Draft 2020-12) hashes.
6
+ module Schemas
7
+ THEME = { "enum" => %w[light dark] }.freeze
8
+ LOCALE = { "type" => "string" }.freeze
9
+ APP_PAGE_WIDTH = { "enum" => %w[full centered] }.freeze
10
+ CONTRAST_MODE = { "type" => "boolean" }.freeze
11
+ STRIPED_ROWS = { "type" => "boolean" }.freeze
12
+ APP_DENSITY = { "enum" => %w[compact standard comfortable] }.freeze
13
+
14
+ COLUMNS = { "type" => "array", "items" => { "type" => "string" } }.freeze
15
+
16
+ COLUMN_WIDTHS = {
17
+ "type" => "object",
18
+ "additionalProperties" => { "type" => "number", "minimum" => 0 },
19
+ }.freeze
20
+
21
+ SORT = {
22
+ "type" => "object",
23
+ "required" => %w[field order],
24
+ "properties" => {
25
+ "field" => { "type" => "string" },
26
+ "order" => { "enum" => %w[ASC DESC] },
27
+ },
28
+ }.freeze
29
+
30
+ SAVED_QUERIES = {
31
+ "type" => "array",
32
+ "items" => {
33
+ "type" => "object",
34
+ "required" => %w[label],
35
+ "properties" => {
36
+ "label" => { "type" => "string" },
37
+ "value" => {
38
+ "type" => "object",
39
+ "properties" => {
40
+ "filter" => { "type" => "object" },
41
+ "displayedFilters" => { "type" => "object", "additionalProperties" => { "type" => "boolean" } },
42
+ "sort" => { "anyOf" => [SORT, { "type" => "null" }] },
43
+ "perPage" => { "type" => %w[integer null], "minimum" => 1 },
44
+ },
45
+ },
46
+ },
47
+ },
48
+ }.freeze
49
+
50
+ WIDGET_LAYOUT = {
51
+ "type" => "object",
52
+ "required" => %w[x y w h],
53
+ "properties" => {
54
+ "x" => { "type" => "integer", "minimum" => 0 },
55
+ "y" => { "type" => "integer", "minimum" => 0 },
56
+ "w" => { "type" => "integer", "minimum" => 1 },
57
+ "h" => { "type" => "integer", "minimum" => 1 },
58
+ },
59
+ }.freeze
60
+
61
+ GROUP_BY_BUCKET = {
62
+ "oneOf" => [
63
+ {
64
+ "type" => "object",
65
+ "required" => %w[type granularity],
66
+ "properties" => {
67
+ "type" => { "const" => "trunc" },
68
+ "granularity" => { "enum" => %w[second minute hour day week month quarter year] },
69
+ },
70
+ },
71
+ {
72
+ "type" => "object",
73
+ "required" => %w[type size],
74
+ "properties" => {
75
+ "type" => { "const" => "width" },
76
+ "size" => { "type" => "number" },
77
+ },
78
+ },
79
+ ],
80
+ }.freeze
81
+
82
+ GROUP_BY_ITEM = {
83
+ "type" => "object",
84
+ "required" => %w[field],
85
+ "properties" => {
86
+ "field" => { "type" => "string" },
87
+ "bucket" => { "anyOf" => [GROUP_BY_BUCKET, { "type" => "null" }] },
88
+ },
89
+ }.freeze
90
+
91
+ CHART_OPTIONS = {
92
+ "type" => "object",
93
+ "properties" => {
94
+ "orientation" => { "enum" => %w[vertical horizontal] },
95
+ "variant" => { "enum" => %w[pie doughnut] },
96
+ },
97
+ }.freeze
98
+
99
+ WIDGET_CONFIG = {
100
+ "type" => "object",
101
+ "required" => %w[id type layout],
102
+ "properties" => {
103
+ "id" => { "type" => "string" },
104
+ "type" => { "type" => "string" },
105
+ "resource" => { "type" => %w[string null] },
106
+ "label" => { "type" => %w[string null] },
107
+ "layout" => WIDGET_LAYOUT,
108
+ "filters" => { "type" => %w[object null], "additionalProperties" => { "type" => "string" } },
109
+ "sort" => { "anyOf" => [SORT, { "type" => "null" }] },
110
+ "perPage" => { "type" => %w[integer null], "minimum" => 1 },
111
+ "columns" => { "type" => %w[array null], "items" => { "type" => "string" } },
112
+ "field" => { "type" => %w[string null] },
113
+ "aggregateFunction" => { "type" => %w[string null], "enum" => [nil, "count", "sum", "avg", "min", "max"] },
114
+ "color" => { "type" => %w[string null], "enum" => [nil, "primary", "secondary", "success", "error", "warning", "info", "inherit"] },
115
+ "groupBy" => { "type" => %w[array null], "items" => GROUP_BY_ITEM },
116
+ "chartOptions" => { "anyOf" => [CHART_OPTIONS, { "type" => "null" }] },
117
+ },
118
+ }.freeze
119
+
120
+ DASHBOARD = {
121
+ "type" => "object",
122
+ "properties" => {
123
+ "widgets" => { "type" => "array", "items" => WIDGET_CONFIG },
124
+ },
125
+ }.freeze
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,19 @@
1
+ module Habarico
2
+ module Contrib
3
+ module Preferences
4
+ # Serializes a ResolvedPreference into the API item shape { key, value, updated_at }.
5
+ # Mirrors the Python UserPreferenceItemSerializer.
6
+ module Serializer
7
+ module_function
8
+
9
+ def item(resolved)
10
+ {
11
+ "key" => resolved.key,
12
+ "value" => resolved.value,
13
+ "updated_at" => resolved.updated_at&.iso8601,
14
+ }
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end