cafe_car 0.1.0 → 0.1.2

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 (240) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +682 -8
  3. data/Rakefile +21 -0
  4. data/app/assets/fonts/Lexend.css +7 -0
  5. data/app/assets/fonts/Lexend.ttf +0 -0
  6. data/app/assets/images/noise.svg +16 -0
  7. data/app/assets/stylesheets/actiontext.css +31 -0
  8. data/app/assets/stylesheets/application.css +1 -0
  9. data/app/assets/stylesheets/cafe_car/code/base16-dark.css +89 -0
  10. data/app/assets/stylesheets/cafe_car/code/base16-light.css +90 -0
  11. data/app/assets/stylesheets/cafe_car/pagination.css +5 -0
  12. data/app/assets/stylesheets/cafe_car/themes/cool.css +32 -0
  13. data/app/assets/stylesheets/cafe_car/themes/cool2.css +31 -0
  14. data/app/assets/stylesheets/cafe_car/themes/defaults.css +60 -0
  15. data/app/assets/stylesheets/cafe_car/themes/warm-dark.css +29 -0
  16. data/app/assets/stylesheets/cafe_car/themes/warm.css +24 -0
  17. data/app/assets/stylesheets/cafe_car/tooltips.css +20 -0
  18. data/app/assets/stylesheets/cafe_car/trix.css +56 -0
  19. data/app/assets/stylesheets/cafe_car/utility.css +63 -0
  20. data/app/assets/stylesheets/cafe_car.css +96 -0
  21. data/app/assets/stylesheets/iconoir.css +22 -0
  22. data/app/assets/stylesheets/ui/Alert.css +25 -0
  23. data/app/assets/stylesheets/ui/Article.css +11 -0
  24. data/app/assets/stylesheets/ui/Button.css +42 -0
  25. data/app/assets/stylesheets/ui/Card.css +74 -0
  26. data/app/assets/stylesheets/ui/Chat.css +33 -0
  27. data/app/assets/stylesheets/ui/Close.css +11 -0
  28. data/app/assets/stylesheets/ui/Code.css +4 -0
  29. data/app/assets/stylesheets/ui/Controls.css +16 -0
  30. data/app/assets/stylesheets/ui/Error.css +3 -0
  31. data/app/assets/stylesheets/ui/Example.css +45 -0
  32. data/app/assets/stylesheets/ui/Field.css +31 -0
  33. data/app/assets/stylesheets/ui/Grid.css +6 -0
  34. data/app/assets/stylesheets/ui/Group.css +16 -0
  35. data/app/assets/stylesheets/ui/Icon.css +27 -0
  36. data/app/assets/stylesheets/ui/Image.css +14 -0
  37. data/app/assets/stylesheets/ui/InfoCircle.css +11 -0
  38. data/app/assets/stylesheets/ui/Input.css +36 -0
  39. data/app/assets/stylesheets/ui/Layout.css +100 -0
  40. data/app/assets/stylesheets/ui/Menu.css +38 -0
  41. data/app/assets/stylesheets/ui/Modal.css +26 -0
  42. data/app/assets/stylesheets/ui/Navigation.css +37 -0
  43. data/app/assets/stylesheets/ui/Page.css +105 -0
  44. data/app/assets/stylesheets/ui/Row.css +9 -0
  45. data/app/assets/stylesheets/ui/Table.css +101 -0
  46. data/app/assets/stylesheets/ui/components.css +24 -0
  47. data/app/controllers/cafe_car/application_controller.rb +9 -0
  48. data/app/controllers/cafe_car/examples_controller.rb +22 -0
  49. data/app/controllers/cafe_car/sessions_controller.rb +30 -0
  50. data/app/controllers/concerns/cafe_car/authentication.rb +61 -0
  51. data/app/javascript/application.js +5 -0
  52. data/app/javascript/cafe_car.js +174 -0
  53. data/app/models/cafe_car/session.rb +18 -0
  54. data/app/policies/cafe_car/application_policy.rb +42 -0
  55. data/app/policies/cafe_car/session_policy.rb +19 -0
  56. data/app/presenters/cafe_car/action_text/rich_text_presenter.rb +7 -0
  57. data/app/presenters/cafe_car/active_record/base_presenter.rb +6 -0
  58. data/app/presenters/cafe_car/active_record/relation_presenter.rb +17 -0
  59. data/app/presenters/cafe_car/active_storage/attached/one_presenter.rb +9 -0
  60. data/app/presenters/cafe_car/active_storage/attachment_presenter.rb +18 -0
  61. data/app/presenters/cafe_car/basic_object_presenter.rb +5 -0
  62. data/app/presenters/cafe_car/code_presenter.rb +18 -0
  63. data/app/presenters/cafe_car/currency_presenter.rb +5 -0
  64. data/app/presenters/cafe_car/date_and_time/compatibility_presenter.rb +6 -0
  65. data/app/presenters/cafe_car/date_presenter.rb +5 -0
  66. data/app/presenters/cafe_car/date_time_presenter.rb +11 -0
  67. data/app/presenters/cafe_car/enumerable_presenter.rb +13 -0
  68. data/app/presenters/cafe_car/false_class_presenter.rb +5 -0
  69. data/app/presenters/cafe_car/hash_presenter.rb +6 -0
  70. data/app/presenters/cafe_car/nil_class_presenter.rb +13 -0
  71. data/app/presenters/cafe_car/presenter.rb +157 -0
  72. data/app/presenters/cafe_car/range_presenter.rb +16 -0
  73. data/app/presenters/cafe_car/record_presenter.rb +5 -0
  74. data/app/presenters/cafe_car/string_presenter.rb +20 -0
  75. data/app/presenters/cafe_car/symbol_presenter.rb +5 -0
  76. data/app/presenters/cafe_car/true_class_presenter.rb +5 -0
  77. data/app/ui/cafe_car/ui/button.rb +9 -0
  78. data/app/ui/cafe_car/ui/card.rb +18 -0
  79. data/app/ui/cafe_car/ui/field.rb +11 -0
  80. data/app/ui/cafe_car/ui/grid.rb +30 -0
  81. data/app/ui/cafe_car/ui/layout.rb +7 -0
  82. data/app/ui/cafe_car/ui/page.rb +14 -0
  83. data/app/views/application/_actions.html.haml +1 -0
  84. data/app/views/application/_alerts.html.haml +2 -0
  85. data/app/views/application/_body.html.haml +7 -0
  86. data/app/views/application/_controls.html.haml +12 -0
  87. data/app/views/application/_debug.html.haml +18 -0
  88. data/app/views/application/_empty.html.haml +1 -0
  89. data/app/views/application/_errors.html.haml +4 -0
  90. data/app/views/application/_field.html.haml +5 -0
  91. data/app/views/application/_fields.html.haml +1 -0
  92. data/app/views/application/_filters.html.haml +8 -0
  93. data/app/views/application/_form.html.haml +6 -0
  94. data/app/views/application/_grid.html.haml +3 -0
  95. data/app/views/application/_grid_item.html.haml +1 -0
  96. data/app/views/application/_head.html.haml +17 -0
  97. data/app/views/application/_index.html.haml +8 -0
  98. data/app/views/application/_index_actions.html.haml +7 -0
  99. data/app/views/application/_navigation.html.haml +9 -0
  100. data/app/views/application/_navigation_links.html.haml +5 -0
  101. data/app/views/application/_notes.html.haml +10 -0
  102. data/app/views/application/_popup.html.haml +7 -0
  103. data/app/views/application/_show.html.haml +9 -0
  104. data/app/views/application/_submit.html.haml +1 -0
  105. data/app/views/application/_table.html.haml +6 -0
  106. data/app/views/cafe_car/application/create.turbo_stream.haml +2 -0
  107. data/app/views/cafe_car/application/destroy.turbo_stream.haml +1 -0
  108. data/app/views/cafe_car/application/edit.html.haml +18 -0
  109. data/app/views/cafe_car/application/edit.turbo_stream.haml +7 -0
  110. data/app/views/cafe_car/application/index.html.haml +15 -0
  111. data/app/views/cafe_car/application/new.html.haml +8 -0
  112. data/app/views/cafe_car/application/new.turbo_stream.haml +8 -0
  113. data/app/views/cafe_car/application/show.html.haml +36 -0
  114. data/app/views/cafe_car/application/update.turbo_stream.haml +2 -0
  115. data/app/views/cafe_car/examples/_example.html.haml +12 -0
  116. data/app/views/cafe_car/examples/_index.html.haml +16 -0
  117. data/app/views/cafe_car/examples/_navigation_links.html.haml +2 -0
  118. data/app/views/cafe_car/examples/ui/_alert.html.haml +4 -0
  119. data/app/views/cafe_car/examples/ui/_button.html.haml +3 -0
  120. data/app/views/cafe_car/examples/ui/_card.html.haml +6 -0
  121. data/app/views/cafe_car/examples/ui/_chat.html.haml +3 -0
  122. data/app/views/cafe_car/examples/ui/_controls.html.haml +3 -0
  123. data/app/views/cafe_car/examples/ui/_error.html.haml +1 -0
  124. data/app/views/cafe_car/examples/ui/_field.html.haml +9 -0
  125. data/app/views/cafe_car/examples/ui/_grid.html.haml +11 -0
  126. data/app/views/cafe_car/examples/ui/_group.html.haml +21 -0
  127. data/app/views/cafe_car/examples/ui/_info_circle.html.haml +1 -0
  128. data/app/views/cafe_car/examples/ui/_menu.html.haml +5 -0
  129. data/app/views/cafe_car/examples/ui/_modal.html.haml +4 -0
  130. data/app/views/cafe_car/examples/ui/_navigation.html.haml +4 -0
  131. data/app/views/cafe_car/examples/ui/_page.html.haml +4 -0
  132. data/app/views/cafe_car/examples/ui/_table.html.haml +13 -0
  133. data/app/views/cafe_car/layouts/mailer.html.haml +8 -0
  134. data/app/views/cafe_car/layouts/mailer.text.erb +1 -0
  135. data/app/views/layouts/action_text/contents/_content.html.erb +3 -0
  136. data/app/views/layouts/application.html.haml +4 -0
  137. data/app/views/layouts/mailer.html.erb +13 -0
  138. data/app/views/layouts/mailer.text.erb +1 -0
  139. data/app/views/notes/_fields.html.haml +1 -0
  140. data/app/views/passwords_mailer/reset.html.haml +5 -0
  141. data/app/views/passwords_mailer/reset.text.erb +4 -0
  142. data/app/views/ui/_card.html.haml +8 -0
  143. data/app/views/ui/_field.html.haml +1 -0
  144. data/app/views/ui/_modal_close.html.haml +1 -0
  145. data/app/views/ui/_page.html.haml +7 -0
  146. data/config/brakeman.ignore +77 -0
  147. data/config/importmap.rb +12 -0
  148. data/config/locales/en.yml +63 -0
  149. data/config/routes.rb +9 -0
  150. data/db/migrate/20251005220017_create_slugs.rb +13 -0
  151. data/lib/cafe_car/active_record.rb +21 -0
  152. data/lib/cafe_car/application_responder.rb +17 -0
  153. data/lib/cafe_car/attributes.rb +23 -0
  154. data/lib/cafe_car/auto_resolver.rb +49 -0
  155. data/lib/cafe_car/caching.rb +20 -0
  156. data/lib/cafe_car/component.rb +155 -0
  157. data/lib/cafe_car/context.rb +17 -0
  158. data/lib/cafe_car/controller/filtering.rb +30 -0
  159. data/lib/cafe_car/controller.rb +218 -0
  160. data/lib/cafe_car/core_ext/array.rb +24 -0
  161. data/lib/cafe_car/core_ext/hash.rb +15 -0
  162. data/lib/cafe_car/core_ext/module.rb +15 -0
  163. data/lib/cafe_car/core_ext.rb +5 -0
  164. data/lib/cafe_car/current.rb +9 -0
  165. data/lib/cafe_car/engine.rb +107 -0
  166. data/lib/cafe_car/field_builder.rb +44 -0
  167. data/lib/cafe_car/field_info.rb +144 -0
  168. data/lib/cafe_car/fields.rb +21 -0
  169. data/lib/cafe_car/filter/field_builder.rb +4 -0
  170. data/lib/cafe_car/filter/field_info.rb +22 -0
  171. data/lib/cafe_car/filter/form_builder.rb +21 -0
  172. data/lib/cafe_car/filter.rb +5 -0
  173. data/lib/cafe_car/filter_builder.rb +20 -0
  174. data/lib/cafe_car/form_builder.rb +105 -0
  175. data/lib/cafe_car/generators.rb +30 -0
  176. data/lib/cafe_car/helpers.rb +178 -0
  177. data/lib/cafe_car/href_builder.rb +97 -0
  178. data/lib/cafe_car/informable.rb +9 -0
  179. data/lib/cafe_car/input_builder.rb +25 -0
  180. data/lib/cafe_car/inputs/association_builder.rb +6 -0
  181. data/lib/cafe_car/inputs/base_input.rb +19 -0
  182. data/lib/cafe_car/inputs/belongs_to_builder.rb +6 -0
  183. data/lib/cafe_car/inputs/password_input.rb +7 -0
  184. data/lib/cafe_car/inputs/string_input.rb +7 -0
  185. data/lib/cafe_car/link_builder.rb +65 -0
  186. data/lib/cafe_car/model.rb +23 -0
  187. data/lib/cafe_car/model_info.rb +24 -0
  188. data/lib/cafe_car/name_patch.rb +17 -0
  189. data/lib/cafe_car/navigation.rb +76 -0
  190. data/lib/cafe_car/option_helpers.rb +53 -0
  191. data/lib/cafe_car/param_parser.rb +45 -0
  192. data/lib/cafe_car/pluralization.rb +15 -0
  193. data/lib/cafe_car/policy.rb +77 -0
  194. data/lib/cafe_car/proc_helpers.rb +13 -0
  195. data/lib/cafe_car/query_builder.rb +186 -0
  196. data/lib/cafe_car/queryable.rb +29 -0
  197. data/lib/cafe_car/resolver.rb +27 -0
  198. data/lib/cafe_car/routing.rb +17 -0
  199. data/lib/cafe_car/table/body_builder.rb +12 -0
  200. data/lib/cafe_car/table/builder.rb +52 -0
  201. data/lib/cafe_car/table/foot_builder.rb +14 -0
  202. data/lib/cafe_car/table/head_builder.rb +26 -0
  203. data/lib/cafe_car/table/label_builder.rb +48 -0
  204. data/lib/cafe_car/table/objects_builder.rb +8 -0
  205. data/lib/cafe_car/table/row_builder.rb +39 -0
  206. data/lib/cafe_car/table_builder.rb +13 -0
  207. data/lib/cafe_car/turbo_tag_builder.rb +7 -0
  208. data/lib/cafe_car/ui.rb +11 -0
  209. data/lib/cafe_car/version.rb +1 -1
  210. data/lib/cafe_car/visitors.rb +21 -0
  211. data/lib/cafe_car.rb +25 -168
  212. data/lib/generators/cafe_car/controller/USAGE +11 -0
  213. data/lib/generators/cafe_car/controller/controller_generator.rb +26 -0
  214. data/lib/generators/cafe_car/controller/templates/controller.rb.tt +5 -0
  215. data/lib/generators/cafe_car/install/USAGE +8 -0
  216. data/lib/generators/cafe_car/install/install_generator.rb +46 -0
  217. data/lib/generators/cafe_car/install/templates/application_policy.rb.tt +7 -0
  218. data/lib/generators/cafe_car/notes/USAGE +12 -0
  219. data/lib/generators/cafe_car/notes/notes_generator.rb +13 -0
  220. data/lib/generators/cafe_car/notes/templates/create_notes.rb.tt +12 -0
  221. data/lib/generators/cafe_car/notes/templates/notable.rb.tt +7 -0
  222. data/lib/generators/cafe_car/notes/templates/note.rb.tt +6 -0
  223. data/lib/generators/cafe_car/policy/USAGE +8 -0
  224. data/lib/generators/cafe_car/policy/policy_generator.rb +39 -0
  225. data/lib/generators/cafe_car/policy/templates/policy.rb.tt +20 -0
  226. data/lib/generators/cafe_car/resource/USAGE +13 -0
  227. data/lib/generators/cafe_car/resource/resource_generator.rb +32 -0
  228. data/lib/generators/cafe_car/sessions/USAGE +17 -0
  229. data/lib/generators/cafe_car/sessions/sessions_generator.rb +29 -0
  230. data/lib/generators/cafe_car/sessions/templates/create_sessions.rb.tt +12 -0
  231. data/lib/tasks/holdco_tasks.rake +532 -0
  232. data/lib/tasks/templates/tasks_header.md +37 -0
  233. metadata +444 -21
  234. data/app/views/cafe_car/application/_fields.html.erb +0 -7
  235. data/app/views/cafe_car/application/_filters.html.erb +0 -0
  236. data/app/views/cafe_car/application/_form.html.erb +0 -22
  237. data/lib/cafe_car/railtie.rb +0 -4
  238. /data/app/views/{cafe_car/application/_actions.html.erb → application/_aside.html.haml} +0 -0
  239. /data/app/views/{cafe_car/application/_aside.html.erb → application/_footer.html.haml} +0 -0
  240. /data/app/views/cafe_car/{application/_extra_fields.html.erb → examples/_index_actions.html.haml} +0 -0
@@ -0,0 +1,19 @@
1
+ module CafeCar
2
+ module Inputs
3
+ class BaseInput
4
+ attr_accessor :options
5
+
6
+ def initialize(template:, **options)
7
+ @template = template
8
+ @options = options
9
+ end
10
+
11
+ def tag = :input
12
+ def type = :text
13
+
14
+ def to_html
15
+ @template.Input(tag:, type:, **options)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,6 @@
1
+ module CafeCar
2
+ module Inputs
3
+ class BelongsToBuilder < AssociationBuilder
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,7 @@
1
+ module CafeCar
2
+ module Inputs
3
+ class StringInput < BaseInput
4
+ def tag = :input
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module CafeCar
2
+ module Inputs
3
+ class StringInput < BaseInput
4
+ def type = :text
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,65 @@
1
+ module CafeCar
2
+ class LinkBuilder
3
+ attr_reader :object
4
+
5
+ delegate :capture, :link_to, :link_to_unless, :current_page?, :href_for, to: :@template
6
+ delegate :model, :policy, to: :p
7
+
8
+ def initialize(template, object, namespace: template.namespace)
9
+ @template = template
10
+ @object = object
11
+ @namespace = namespace
12
+ end
13
+
14
+ def p = @template.present(@object)
15
+ def can?(action) = policy.public_send("#{action}?")
16
+ def cant?(action) = !can?(action) && disabled(action, :policy)
17
+
18
+ def i18n(*, scope: nil, **) = p.i18n(*, scope: [ :controls, *scope ], **)
19
+
20
+ def confirm(key) = i18n(key, scope: :confirm)
21
+ def disabled(action, reason) = i18n(action, scope: [ :disabled, reason ])
22
+
23
+ def turbo!(opts)
24
+ opts.replace({
25
+ data: {
26
+ turbo_stream: true,
27
+ turbo_method: opts.delete(:method),
28
+ turbo_confirm: opts.delete(:confirm)
29
+ }
30
+ }.deep_merge(opts))
31
+ end
32
+
33
+ def link(action, target, label = i18n(action), disabled: false, hide: false, params: nil, **opts, &)
34
+ params ||= {}
35
+ disabled ||= cant?(action)
36
+ return if disabled and hide
37
+
38
+ href = href_for(*target, action:, namespace: @namespace, **params)
39
+ current = current_page?(href, check_parameters: true)
40
+ in_link = @template.context?(:a)
41
+ content = block_given? ? capture(label, &) : label
42
+
43
+ link_to_unless(disabled || current || in_link, content, href, **turbo!(opts)) do
44
+ @template.tag.span(content, class: "disabled", disabled: true, title: disabled.presence)
45
+ end
46
+ end
47
+
48
+ def show(*, **, &) = link(:show, @object, *, data: { turbo_stream: nil }, **, &)
49
+ def edit(...) = link(:edit, @object, ...)
50
+ def destroy(*, **, &) = link(:destroy, @object, *, method: :delete, confirm: confirm(:destroy), **, &)
51
+ def index(*, **, &) = link(:index, model, *, hide: true, data: { turbo_stream: nil }, **, &)
52
+ def new(*, **, &) = link(:new, model, *, hide: true, **, &)
53
+
54
+ def code(path = nil)
55
+ return unless Rails.env.development?
56
+ return unless @template.request.local?
57
+ path ||= caller_locations(1, 1).first.path
58
+
59
+ link_to "✎", "rubymine://open?file=#{path}" # &line=%{line}
60
+ end
61
+
62
+ def html_safe? = true
63
+ def to_s = p.to_s
64
+ end
65
+ end
@@ -0,0 +1,23 @@
1
+ module CafeCar::Model
2
+ extend ActiveSupport::Concern
3
+
4
+ include CafeCar::Queryable
5
+ include CafeCar::Informable
6
+
7
+ class_methods do
8
+ def sorted(*args)
9
+ return all if args.compact_blank!.empty?
10
+ args = args.flat_map { normalize_sort_key(_1) }
11
+ reorder(*args)
12
+ end
13
+
14
+ def normalize_sort_key(key)
15
+ case key
16
+ when /,/
17
+ key.split(",").map { normalize_sort_key _1 }
18
+ when /^-(.+)$/ then { $1 => :desc }
19
+ else key
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,24 @@
1
+ module CafeCar
2
+ class ModelInfo
3
+ include Caching
4
+
5
+ attr_reader :model
6
+
7
+ def self.find(object)
8
+ model = object.is_a?(Class) ? object : object.class
9
+ @cache ||= {}
10
+ @cache[model] ||= new(model:)
11
+ end
12
+
13
+ def initialize(model:)
14
+ @model = model
15
+ @field = {}
16
+ end
17
+
18
+ def columns = model.column_names
19
+ def field_names = model.column_names | model.reflect_on_all_attachments.map(&:name)
20
+ def field(method) = @field[method] ||= FieldInfo.new(model:, method:)
21
+
22
+ derive :fields, -> { Fields.new(field_names.map { field _1 }) }
23
+ end
24
+ end
@@ -0,0 +1,17 @@
1
+ module CafeCar
2
+ module NamePatch
3
+ def self.patch!
4
+ ActiveModel::Name.class_eval do
5
+ def human(options = {})
6
+ return @human if i18n_keys.empty? || i18n_scope.empty?
7
+
8
+ key, *defaults = i18n_keys
9
+ defaults << options[:default] if options[:default]
10
+ defaults << @human
11
+
12
+ I18n.translate(key, scope: i18n_scope, count: 1, **options, default: defaults)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,76 @@
1
+ module CafeCar
2
+ class Navigation
3
+ class Route
4
+ delegate :tag, :ui, :ui_class, :capture, :concat, :t, to: :@template
5
+ delegate :name, :requirements, to: :@route
6
+
7
+ def initialize(route, template:)
8
+ @route = route
9
+ @template = template
10
+ end
11
+
12
+ def controller = requirements[:controller]
13
+ def action = requirements[:action]
14
+ def index? = action == "index"
15
+ def rails? = name =~ /rails/
16
+ def group = controller.split(?/)[..-2]
17
+ def text = controller.split(?/).last
18
+ def params = requirements.clone.tap do |p|
19
+ p[:controller] = "/" + p[:controller]
20
+ end
21
+
22
+ def icon_name = t(text, scope: "navigation.icon", default: nil)&.to_sym
23
+ def icon = @template.icon(icon_name, :before)
24
+
25
+ def content
26
+ capture do
27
+ concat icon
28
+ concat text.titleize
29
+ end
30
+ end
31
+
32
+ def link(**opts)
33
+ ui.Navigation().Link(href: @template.href_for([ params ]), **opts) { content }
34
+ end
35
+ end
36
+
37
+ def initialize(template, **options)
38
+ @template = template
39
+ @options = options
40
+ end
41
+
42
+ delegate :ui_class, to: :@template
43
+
44
+ def router = Rails.application.routes.router
45
+ def named_routes = Rails.application.routes.named_routes.to_h.values.map { Route.new(_1, template: @template) }
46
+ def index_routes = named_routes.select(&:index?)
47
+ def groups = routes.group_by(&:group)
48
+
49
+ def routes
50
+ @routes ||= index_routes.reject(&:rails?)
51
+ .uniq(&:requirements)
52
+ end
53
+
54
+ def recognize(obj, **)
55
+ req = case obj
56
+ when String
57
+ path = ActionDispatch::Journey::Router::Utils.normalize_path(path)
58
+ env = Rack::MockRequest.env_for(path, method: :get, **)
59
+ ActionDispatch::Request.new(env)
60
+ when ActionDispatch::Request then obj
61
+ else raise "cannot recognize this obj"
62
+ end
63
+
64
+ router.recognize(req) do |route, params|
65
+ return Route.new(route, template: @template)
66
+ end
67
+ end
68
+
69
+ def current = recognize(@template.request)
70
+
71
+ def link_to(*args, **opts, &block)
72
+ block ||= -> { @template.tag.span(_1, class: ui_class([ :navigation, :link ], :current)) }
73
+ @template.link_to_unless_current(*args, class: ui_class([ :navigation, :link ]), **opts, &block)
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,53 @@
1
+ module CafeCar
2
+ module OptionHelpers
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ class_attribute :option_defaults, default: {}
7
+ end
8
+
9
+ def get_options = @options
10
+
11
+ def assign_options!
12
+ raise "@options is not a hash" unless get_options.is_a? Hash
13
+ option_defaults.each { assign_option!(_1, _2) }
14
+ end
15
+
16
+ def assign_option!(k, default)
17
+ value = get_options.delete(k) { default.respond_to?(:call) ? default.call : default.clone }
18
+ instance_variable_set("@#{k}", value)
19
+ end
20
+
21
+ class_methods do
22
+ def inherited(subclass)
23
+ super
24
+ subclass.option_defaults = option_defaults.deep_dup
25
+ end
26
+
27
+ def option(*names, default: nil, accessor: true, reader: accessor, writer: accessor, presence: accessor, macro: accessor)
28
+ names.each do |name|
29
+ if respond_to?(name) and reader
30
+ raise ArgumentError, "Option name #{name} conflicts with existing method"
31
+ end
32
+ attr_reader name if reader
33
+ attr_writer name if writer
34
+ option_defaults[name] = default
35
+ define_method("#{name}?") { instance_variable_get("@#{name}").present? } if presence
36
+ define_singleton_method(name) { |v| option_defaults[name] = v } if macro
37
+ end
38
+ end
39
+
40
+ def flag(*names, setter: true, **)
41
+ names.each do |name|
42
+ option(name, default: false, reader: false, **)
43
+ define_method(name) { instance_variable_set("@#{name}", true); self } if setter
44
+ end
45
+ end
46
+
47
+ def option_accessor(name)
48
+ define_method(name) { get_options[name] }
49
+ define_method("#{name}=") { get_options[name] = _1 }
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,45 @@
1
+ class CafeCar::ParamParser
2
+ def initialize(params)
3
+ @params = params
4
+ end
5
+
6
+ def parsed
7
+ @parsed ||= @params.compact_blank
8
+ .then { params _1 }
9
+ end
10
+
11
+ def params(params)
12
+ params.map { |k, v| k.split(".").reverse.reduce(value(v)) { { _2 => _1 } } }
13
+ .reduce({}) { _1.deep_merge(_2, &method(:merge)) }
14
+ .with_indifferent_access
15
+ end
16
+
17
+ def merge(_, a, b)
18
+ if a.is_a?(Array) || b.is_a?(Array)
19
+ [ *Array.wrap(a), *Array.wrap(b) ]
20
+ else
21
+ b
22
+ end
23
+ end
24
+
25
+ def value(v)
26
+ case v
27
+ when Array then v.map { value(_1) }
28
+ when Hash then params(v).tap { _1.merge!(_1.delete("")) if _1[""] }
29
+ when '""', "''" then ""
30
+ when "nil", "" then nil
31
+ when /[{}\[\]]/ then value(JSON.parse(v))
32
+ when /,/ then value(v.split(","))
33
+ when /^(.*?)\.\.(\.?)(.*)$/
34
+ begin
35
+ Range.new(value($1), value($3), $2.present?)
36
+ rescue ArgumentError
37
+ v
38
+ end
39
+ when /^\$(\w+)\.(\w+)$/
40
+ # TODO: make less scary
41
+ $1.constantize.arel_table[$2]
42
+ else v
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,15 @@
1
+ module CafeCar
2
+ module Pluralization
3
+ def pluralize(locale, entry, count)
4
+ return super unless entry.is_a?(String) && count
5
+ count = 1 if count == :one
6
+ count = 2 unless count.is_a?(Integer)
7
+ entry.pluralize(count)
8
+ end
9
+
10
+ def localize(locale, object, format = :default, options = {})
11
+ options[:ordinal] ||= object.day.ordinalize
12
+ super
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,77 @@
1
+ module CafeCar::Policy
2
+ extend ActiveSupport::Concern
3
+
4
+ def policy(object) = Pundit.policy(user, object)
5
+
6
+ def model
7
+ @model ||= object.try(:klass) or object.is_a?(Class) ? object : object.class
8
+ end
9
+
10
+ def info(method)
11
+ @info ||= {}
12
+ @info[method] ||= CafeCar[:FieldInfo].new(model:, method:)
13
+ end
14
+
15
+ def title_attribute
16
+ @title_attribute ||= displayable_attributes.first
17
+ end
18
+
19
+ def logo_attribute
20
+ model.info.fields.listable.attachments.first&.method
21
+ end
22
+
23
+ def listable_attributes
24
+ model.info.fields.listable.map(&:method)
25
+ end
26
+
27
+ def displayable_attributes
28
+ permitted_attribute_keys
29
+ .union(model.columns.map(&:name).map(&:to_sym))
30
+ .map { association_for_attribute(_1) || _1 }
31
+ .reject { filtered_attribute? _1 } - %i[id]
32
+ end
33
+
34
+ def permitted_fields
35
+ @permitted_fields ||= permitted_attribute_keys.map { info _1 }
36
+ end
37
+
38
+ def editable_attributes
39
+ permitted_fields.map(&:input_key) - permitted_fields.flat_map(&:abrogated_keys)
40
+ end
41
+
42
+ def displayable_associations
43
+ model.reflections.values
44
+ .select { |a| !a.options[:autosave] && !a.options[:polymorphic] }
45
+ .reject { _1.class_name =~ /^ActiveStorage::/ }
46
+ .map { _1.name.to_sym }
47
+ end
48
+
49
+ def permitted_attribute_keys
50
+ permitted_attributes.flat_map { |a| a.try(:keys) || a }
51
+ end
52
+
53
+ def permitted_association?(name)
54
+ ref = model.reflect_on_association(name)
55
+
56
+ return false if ref.has_one?
57
+ return permitted_attribute?(ref.foreign_key) if ref.belongs_to?
58
+
59
+ permitted_attribute?("#{ref.name.to_s.singularize}_ids")
60
+ end
61
+
62
+ def filtered_attribute?(attribute)
63
+ model.inspection_filter.filter_param(attribute, nil).present?
64
+ end
65
+
66
+ def permitted_attribute?(attribute)
67
+ permitted_attribute_keys.include?(attribute.to_sym)
68
+ end
69
+
70
+ def displayable_attribute?(attribute)
71
+ displayable_attributes.include?(attribute.to_sym)
72
+ end
73
+
74
+ def association_for_attribute(attribute)
75
+ info(attribute).reflection&.name
76
+ end
77
+ end
@@ -0,0 +1,13 @@
1
+ module CafeCar
2
+ module ProcHelpers
3
+ def call_procs!(options, ...)
4
+ options.each do |k, v|
5
+ options[k] = v.call(...) if v.respond_to? :call
6
+ end
7
+ end
8
+
9
+ def clone_or_call!(value, ...)
10
+ value.respond_to?(:call) ? value.call(...) : value.clone
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,186 @@
1
+ module CafeCar
2
+ class QueryBuilder
3
+ require "activerecord_where_assoc"
4
+
5
+ Op = Struct.new(:op, :rhs) do
6
+ def initialize(op, rhs)
7
+ super(op.to_sym, rhs)
8
+ end
9
+
10
+ def flop = op.to_s.tr("<>", "><").to_sym
11
+ def map = Op.new(op, yield(rhs))
12
+
13
+ def arel(node) = node.public_send(arel_op, rhs)
14
+
15
+ def arel_op
16
+ case op
17
+ when :< then :lt
18
+ when :> then :gt
19
+ when :>= then :gteq
20
+ when :<= then :lteq
21
+ when :== then :eq
22
+ else op
23
+ end
24
+ end
25
+ end
26
+
27
+ attr_reader :scope
28
+
29
+ def initialize(scope)
30
+ @scope = scope
31
+ end
32
+
33
+ def unscoped = QueryBuilder.new(@scope.unscope(:where))
34
+ def arel(key) = @scope.arel_table[chomp(key)]
35
+ def chomp(key) = key.to_s.sub(/\W+$/, "")
36
+
37
+ def parse_time(value)
38
+ Chronic.parse(value, guess: false, context: :past)
39
+ rescue NoMethodError
40
+ nil
41
+ end
42
+
43
+ def parse_range(key, value)
44
+ Range.new(parse_value(key, value.begin).then { _1.try(:begin) or _1 },
45
+ parse_value(key, value.end).then { value.exclude_end? ? _1.try(:begin) : _1.try(:end) or _1 },
46
+ value.exclude_end?)
47
+ end
48
+
49
+ def parse(key, value)
50
+ new_value = parse_value(key, value)
51
+ if new_value != value
52
+ parse(key, new_value)
53
+ else
54
+ new_value
55
+ end
56
+ end
57
+
58
+ def parse_value(key, value)
59
+ case value
60
+ in Op(rhs: /^=(.*)$/)
61
+ Op.new("#{value.op}=", $1)
62
+ in Op(op: (:< | :>=), rhs: Range)
63
+ value.map(&:begin)
64
+ in Op(op: (:> | :<=), rhs: Range)
65
+ value.map(&:end)
66
+ in Range
67
+ parse_range(key, value)
68
+ in Array | Op
69
+ value.map { parse_value(key, _1) }
70
+ in "true" then true
71
+ in "false" then false
72
+ in String
73
+ case column(key)&.type || reflection(key)&.macro
74
+ when :datetime then parse_time(value) || value
75
+ when :integer then value.to_i
76
+ when :float then value.to_f
77
+ when :belongs_to, :has_many, :has_one
78
+ value.to_i
79
+ else value
80
+ end
81
+ else value
82
+ end
83
+ end
84
+
85
+ def update!(&)
86
+ scope = yield @scope
87
+ @scope = scope if scope
88
+ self
89
+ end
90
+
91
+ def not!(&)
92
+ inverted = unscoped.tap { _1.instance_exec(&) }.scope.invert_where
93
+ update! { _1.and(inverted) }
94
+ end
95
+
96
+ def column(name) = @scope.columns_hash[name.to_s]
97
+ def reflection(name) = @scope.reflect_on_association(name)
98
+ def association?(name) = reflection(name).present?
99
+ def attribute?(name) = column(name).present?
100
+ def scope?(name) = name.intern.in? @scope.local_methods
101
+
102
+ def arel!(node) = @scope.where!(node)
103
+
104
+ def param!(key, value)
105
+ case key
106
+ when /^(.*?)\s*!$/
107
+ not! { param!($1, value) }
108
+ when /^(.*?)\s*~$/
109
+ param!($1, Regexp.new(value, Regexp::IGNORECASE))
110
+ when /^(.*?)\s*([<>]=?)$/
111
+ param!($1, Op.new($2, value))
112
+ when method(:association?)
113
+ association!(key, value)
114
+ when method(:attribute?)
115
+ attribute!(key, value)
116
+ when method(:scope?)
117
+ scope!(key, value)
118
+ else
119
+ raise MissingAttributeError, "can't find #{key.inspect} on #{@scope.model_name}"
120
+ end
121
+ end
122
+
123
+ def attribute!(key, value)
124
+ case [ key, value ]
125
+ in _, Regexp
126
+ @scope.where!(arel(key).matches_regexp(value.source, !value.casefold?))
127
+ in _, Op
128
+ @scope.where!(parse(key, value).arel(arel(key)))
129
+ else @scope.where!(key => parse(key, value))
130
+ end
131
+ end
132
+
133
+ def association!(name, value, ...)
134
+ update! do
135
+ case value
136
+ when true then @scope.where_assoc_exists(name)
137
+ when false then @scope.where_assoc_not_exists(name)
138
+ when Integer, Range, /^\d+$/
139
+ @scope.where_assoc_count(parse(name, value), :==, name)
140
+ when Op
141
+ value = parse(name, value)
142
+ @scope.where_assoc_count(value.rhs, value.flop, name)
143
+ else @scope.where_assoc_exists(name) { all.query!(value, ...) }
144
+ end
145
+ end
146
+ end
147
+
148
+ def scope!(name, value)
149
+ value = parse_value(name, value)
150
+ arity = (@scope.scopes[name] || @scope.method(name)).arity
151
+ value = nil if arity == 0 and value == true
152
+
153
+ update! { _1.public_send(name, *value) }
154
+ end
155
+
156
+ def search!(term)
157
+ @scope.search!(term) if @scope.respond_to?(:search!)
158
+ @scope.query!("body~": term) if @scope < ::ActionText::RichText
159
+ update! { _1.search(term) }
160
+ end
161
+
162
+ def query!(params = nil)
163
+ case params
164
+ when Hash then params.each { param!(_1, _2) }
165
+ when Array then params.each { query! _1 }
166
+ when String then search!(params)
167
+ # when Arel::Nodes::Node then arel!(params)
168
+ when nil
169
+ else raise ArgumentError, "cannot query on #{params}"
170
+ end
171
+ self
172
+ end
173
+
174
+ def query(...) = clone.update!(&:all).query!(...)
175
+ end
176
+ end
177
+
178
+ # Article.query do
179
+ # published
180
+ # user { name(/bob/i) }
181
+ # user.name(/bob/i)
182
+ # end
183
+ #
184
+ # Article.query(published: true, user: {name: /bob/i})
185
+ #
186
+ # Article.published(true).where_assoc_exists(:user) { where(name: /bob/i) }
@@ -0,0 +1,29 @@
1
+ module CafeCar::Queryable
2
+ extend ActiveSupport::Concern
3
+
4
+ class_methods do
5
+ def scope(name, body)
6
+ scopes[name] = body
7
+ super
8
+ end
9
+
10
+ def sample = offset(rand(count)).first
11
+
12
+ def query(params) = query_builder.query(params).scope
13
+ def query!(params) = query_builder.query!(params).scope
14
+
15
+ def query_builder
16
+ CafeCar::QueryBuilder.new(all)
17
+ end
18
+
19
+ def scopes
20
+ @scopes ||= {}.with_indifferent_access
21
+ end
22
+
23
+ def local_methods
24
+ @local_methods ||= public_methods -
25
+ ActiveRecord::Base.public_methods -
26
+ Kaminari::ConfigurationMethods::ClassMethods.instance_methods
27
+ end
28
+ end
29
+ end