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,155 @@
1
+ module CafeCar
2
+ class Component
3
+ concerning :Macros do
4
+ included do
5
+ class_attribute :option_defaults, default: {}
6
+ class_attribute :attribute_defaults, default: {}
7
+ end
8
+
9
+ def extract_options!(options)
10
+ @options = options.extract!(*option_defaults.keys).with_defaults!(option_defaults)
11
+ end
12
+
13
+ class_methods do
14
+ def inherited(subclass)
15
+ super
16
+ subclass.option_defaults = option_defaults.deep_dup
17
+ subclass.attribute_defaults = attribute_defaults.deep_dup
18
+ end
19
+
20
+ def component(*names, **, &)
21
+ names.each do |name|
22
+ define_class(name, CafeCar[:Component], **, &)
23
+ end
24
+ end
25
+
26
+ def flag(flag)
27
+ include Module.new do
28
+ define_method(flag) { |v| @options[flag] = v.nil? ? true : v }
29
+ end
30
+ end
31
+
32
+ def option(name, default: nil, accessor: true, reader: accessor, writer: accessor, presence: accessor, macro: accessor)
33
+ option_defaults[name] = default
34
+ define_singleton_method(name) { |v| option_defaults[name] = v } if macro
35
+ include Module.new {
36
+ define_method(name) { options[name] } if reader
37
+ define_method("#{name}=") { |v| options[name] = v } if writer
38
+ define_method("#{name}?") { options[name].present? } if presence
39
+ }
40
+ end
41
+ end
42
+ end
43
+
44
+ def self.method_missing(name, v)
45
+ attribute_defaults[name] = v
46
+ end
47
+
48
+ attr_reader :flags, :options, :attributes
49
+
50
+ delegate :render, :capture, :safe_join, :ui_class, :context?, to: :@template
51
+
52
+ option :tag, default: :div
53
+ option :class, accessor: false
54
+ option :data, default: {}
55
+ option :href
56
+ option :tip
57
+
58
+ def initialize(template, name, *args, **attributes, &block)
59
+ @template = template
60
+ @names = [ *name ].map(&:underscore)
61
+ @flags = args.extract! { _1.is_a? Symbol }
62
+ @args = args.flatten.compact_blank
63
+ @attributes = attributes.with_defaults!(attribute_defaults)
64
+ @children = attributes.extract_if! { _1 =~ /^[A-Z]\w*$/ }
65
+ @block = block
66
+ extract_options!(@attributes)
67
+ end
68
+
69
+ def name = @names.last
70
+ def tag = href? ? :a : super
71
+ def href? = super && !context?(:a) && !current_href?
72
+
73
+ def href
74
+ @template.href_for(super) if href?
75
+ end
76
+
77
+ def current_href? = options[:href]&.then { @template.current_href?(_1, check_parameters: true) }
78
+ def ancestor_href? = options[:href]&.then { @template.ancestor_href?(_1) }
79
+
80
+ def data
81
+ super.merge({ tip: }.compact_blank)
82
+ end
83
+
84
+ def partial? = @template.partial?(partial_name)
85
+ def partial_name = "ui/" + @names.join("_")
86
+
87
+ def render_partial
88
+ render(partial_name, options:, flags:, name => self, **options) { content }
89
+ end
90
+
91
+ def selector = class_names.join(?_).then { ?. + _1 }
92
+
93
+ def class_names = @names.map(&:camelize)
94
+
95
+ def class_name
96
+ @template.ui_class(class_names, *flags, *options[:class], options[:tag].to_s => href?,
97
+ current: current_href?,
98
+ ancestor: ancestor_href? && !current_href?)
99
+ end
100
+
101
+ def attributes
102
+ @attributes.merge(class: class_name, href:, data:)
103
+ end
104
+
105
+ def children
106
+ @children.map { |name, content| send(name) { content } }
107
+ end
108
+
109
+ def blank?
110
+ @block and body and content.blank? || !content.match?(/^.*?[^<\s]/) || content.gsub(/<!--.*?-->/, "").blank?
111
+ end
112
+
113
+ def body
114
+ @body ||= context { partial? ? render_partial : content }
115
+ end
116
+
117
+ def content
118
+ @content ||= safe_join [ *children, *@args, *(capture(self, &@block) if @block) ]
119
+ end
120
+
121
+ def context(&)
122
+ href? ? @template.context(:a, &) : capture(&)
123
+ end
124
+
125
+ def wrapper(&)
126
+ @template.content_tag(tag, **attributes, &)
127
+ end
128
+
129
+ def ~@ = @template.concat(self)
130
+ def +(o) = safe_join([ self, o ])
131
+ def <<(o) = @template.concat(o)
132
+
133
+ def html_safe? = true
134
+
135
+ def to_s = to_html
136
+
137
+ def to_html
138
+ return "" if blank?
139
+ wrapper { body }
140
+ end
141
+
142
+ def child(name, ...)
143
+ c = self.class.try { const_defined?(name) ? const_get(name) : Component }
144
+ c.new(@template, [ *@names, name ], ...)
145
+ end
146
+
147
+ def method_missing(name, ...)
148
+ if name =~ /^[A-Z]/
149
+ child(name, ...)
150
+ else
151
+ super
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,17 @@
1
+ module CafeCar
2
+ class Context
3
+ def initialize(template, prefix: nil)
4
+ @template = template
5
+ @prefix = prefix
6
+ end
7
+
8
+ def class(name, ...) = @template.ui_class([ *@prefix, *name ], ...)
9
+ # def wrapper(...) = Component.new(@template, [*@prefix], ...).wrapper(...)
10
+ def <<(obj) = @template.concat(obj)
11
+
12
+ def method_missing(name, ...)
13
+ c = CafeCar[:UI].const_try(name) || Component
14
+ c.new(@template, [ *@prefix, name ], ...)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,30 @@
1
+ module CafeCar::Controller::Filtering
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ helper_method :parsed_params, :dot_params, :filtered?
6
+ end
7
+
8
+ private
9
+
10
+ def filtered(scope)
11
+ scope.query(parsed_params[""])
12
+ end
13
+
14
+ def filtered?
15
+ parsed_params[""].present?
16
+ end
17
+
18
+ def dot_params
19
+ request.params.slice(*request.params.keys.grep(/^\./))
20
+ end
21
+
22
+ def parsed_params
23
+ @parsed_params ||=
24
+ if request.get? || request.head?
25
+ CafeCar::ParamParser.new(request.params).parsed
26
+ else
27
+ request.params
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,218 @@
1
+ require "responders"
2
+
3
+ module CafeCar
4
+ module Controller
5
+ extend ActiveSupport::Concern
6
+
7
+ include Pundit::Authorization
8
+ include Filtering, Authentication
9
+
10
+ class_methods do
11
+ def model(model)
12
+ define_method(:model) { @model ||= model }
13
+ end
14
+
15
+ def default_view(v = @default_view || "table")
16
+ @default_view = v.to_s
17
+ end
18
+
19
+ def cafe_car(only: nil, except: nil, model: nil)
20
+ _only = ->(actions) do
21
+ actions -= except if except
22
+ actions &= only if only
23
+ actions
24
+ end
25
+
26
+ self.model model if model
27
+
28
+ rescue_from ::ActiveRecord::RecordInvalid,
29
+ ::ActiveModel::ValidationError, with: :render_invalid_object
30
+
31
+ rescue_from ::Pundit::NotAuthorizedError, with: :render_unauthorized
32
+
33
+ append_cafe_car_views
34
+
35
+ respond_to :json, :html, :turbo_stream
36
+
37
+ before_action :set_current_attributes
38
+
39
+ before_action :find_object, only: _only.(%i[show edit update destroy])
40
+ before_action :build_object, only: _only.(%i[new create])
41
+ before_action :find_objects, only: _only.(%i[index])
42
+ before_action :assign_attributes, only: _only.(%i[create update])
43
+ before_action :authorize!
44
+ end
45
+
46
+ def append_cafe_car_views
47
+ append_view_path CafeCar::Engine.root.join("app/views/cafe_car")
48
+ append_view_path "app/views/cafe_car"
49
+ end
50
+
51
+ def define_callbacks_with_helpers(*names, **)
52
+ define_callbacks(*names, **)
53
+
54
+ names.each do |name|
55
+ %i[before around after].each do |kind|
56
+ define_singleton_method "#{kind}_#{name}" do |*args, **opts|
57
+ set_callback(name, kind, *args, prepend: true, **opts)
58
+ end
59
+
60
+ define_singleton_method "skip_#{kind}_#{name}" do |*args|
61
+ skip_callback(name, kind, *args)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ included do
69
+ self.responder = CafeCar[:ApplicationResponder]
70
+ default_form_builder CafeCar[:FormBuilder]
71
+
72
+ define_callbacks_with_helpers :render, :update, :create, :destroy
73
+ add_flash_types :success, :warning, :error
74
+
75
+ helper_method :model, :model_name, :object, :objects
76
+ helper_method :action, :scope, :view, :default_view
77
+
78
+ helper Helpers
79
+ delegate :present, :href_for, :namespace, to: :helpers
80
+
81
+ after_action :verify_authorized, :verify_policy_scoped
82
+ end
83
+
84
+ def index = respond_with objects
85
+ def show = respond_with object
86
+ def new = respond_with object
87
+ def edit = respond_with object
88
+
89
+ def create
90
+ run_callbacks(:create) { object.save! }
91
+ respond_with object
92
+ end
93
+
94
+ def update
95
+ run_callbacks(:update) { object.save! }
96
+ respond_with object
97
+ end
98
+
99
+ def destroy
100
+ run_callbacks(:destroy) { object.destroy! }
101
+ respond_with object
102
+ end
103
+
104
+ def respond_with(*resources, **options, &block)
105
+ super(*namespace, *resources, **options, &block)
106
+ end
107
+
108
+ private
109
+
110
+ def authorize!
111
+ return authorize object if object
112
+ return authorize objects if objects
113
+ raise "nothing to authorize! Define self.object or self.objects"
114
+ end
115
+
116
+ def flash!
117
+ flash[:success] = present(object).i18n("#{action_name}_html", scope: :flashes)
118
+ end
119
+
120
+ def sorted(scope)
121
+ scope.sorted(*params[:sort].presence)
122
+ end
123
+
124
+ def paginated(scope, page: params[:page], per: params[:per])
125
+ scope.page(page).per(per)
126
+ end
127
+
128
+ def build_object
129
+ self.object = scope.new
130
+ end
131
+
132
+ def find_object
133
+ delimiter = model.param_delimiter
134
+ id = unwrap params.extract_value(:id, delimiter:)
135
+ self.object = scope.except(:limit, :offset).find(id)
136
+ end
137
+
138
+ def unwrap(arr) = arr.size == 1 ? arr.first : arr
139
+
140
+ def find_objects
141
+ self.objects = scope
142
+ end
143
+
144
+ def assign_attributes
145
+ object.assign_attributes(permitted_attributes(object))
146
+ end
147
+
148
+ def scope = model.all.then { policy_scope _1 }
149
+ .then { sorted _1 }
150
+ .then { filtered _1 }
151
+ .then { paginated _1 }
152
+ def objects = instance_variable_get("@#{model_name.plural}")
153
+ def objects=(value)
154
+ instance_variable_set("@#{model_name.plural}", value)
155
+ end
156
+
157
+ def object = instance_variable_get("@#{model_name.singular}")
158
+ def object=(value)
159
+ instance_variable_set("@#{model_name.singular}", value)
160
+ end
161
+
162
+ def model_name = model.model_name
163
+
164
+ def model
165
+ @model ||= self.class.name.gsub(/.*::|Controller$/, "")
166
+ .classify
167
+ .then { self.class.module_parent.const_get _1 }
168
+ end
169
+
170
+ def render_invalid_object
171
+ render(object.persisted? ? "edit" : "new", status: :unprocessable_content)
172
+ end
173
+
174
+ def render_unauthorized
175
+ if !sessions_available?
176
+ head :forbidden
177
+ elsif authenticated?
178
+ redirect_back_or_to root_path
179
+ else
180
+ request_authentication
181
+ end
182
+ end
183
+
184
+ # def default_render(...) = run_callbacks(:render) { super }
185
+
186
+ def default_url_options
187
+ {} # { locale: I18n.locale }
188
+ end
189
+
190
+ def action
191
+ @action ||= ActiveSupport::StringInquirer.new(action_name)
192
+ end
193
+
194
+ def default_view = self.class.default_view
195
+ def view
196
+ params.fetch(:view) { default_view }
197
+ end
198
+
199
+ def _render_with_renderer_json(obj, options)
200
+ # permitted_attributes is record-oriented, so ask a record for the column
201
+ # list even when serializing a collection.
202
+ record = obj.is_a?(CafeCar::Model) ? obj : obj.klass.new
203
+ options[:only] ||= [ :id ] | policy(record).displayable_attributes
204
+
205
+ if obj.is_a?(CafeCar::Model)
206
+ options[:include] ||= policy(obj).displayable_associations
207
+ end
208
+
209
+ super obj, **options
210
+ end
211
+
212
+ def set_current_attributes
213
+ CafeCar[:Current].request_id = request.uuid
214
+ CafeCar[:Current].user_agent = request.user_agent
215
+ CafeCar[:Current].ip_address = request.ip
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Array
4
+ def overlap(other)
5
+ 0.upto(size) do |i|
6
+ if other.start_with? drop(i)
7
+ return slice(0, i) + other
8
+ end
9
+ end
10
+ end
11
+
12
+ def extract!(pattern = nil, &block)
13
+ block = -> { pattern === _1 } if pattern
14
+ return to_enum(:extract!) { size } unless block
15
+
16
+ extracted_elements = []
17
+
18
+ reject! do |element|
19
+ extracted_elements << element if block.(element)
20
+ end
21
+
22
+ extracted_elements
23
+ end
24
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Hash
4
+ # Removes and returns a hash containing the key/value pairs for which the
5
+ # block returns a true value given the key.
6
+ #
7
+ # hash = {:a => 1, "b" => 2, :c => 3}
8
+ # hash.extract_if! { _1.is_a? Symbol } #=> {a: 1, c: 3}
9
+ # hash #=> {"b" => 2 }
10
+ def extract_if!
11
+ each_with_object(self.class.new) do |(key, value), result|
12
+ result[key] = delete(key) if yield(key, value)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Module
4
+ # Shorthand for `const_set(name.camelize, Class.new(parent) { ... })`.
5
+ # Useful when defining classes in macros.
6
+ def define_class(name, *, **macros, &)
7
+ name = name.to_s.camelize
8
+ raise NameError, "class exists: #{name}" if const_defined?(name, false)
9
+ klass = Class.new(*) do
10
+ macros.each { |key, value| send(key, *value) }
11
+ class_eval(&) if block_given?
12
+ end
13
+ const_set name, klass
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ Dir.glob(File.expand_path("core_ext/*.rb", __dir__)).sort.each do |path|
4
+ require path
5
+ end
@@ -0,0 +1,9 @@
1
+ module CafeCar
2
+ class Current < ActiveSupport::CurrentAttributes
3
+ delegate :user, to: :session, allow_nil: true
4
+
5
+ attribute :request_id, :user_agent, :ip_address
6
+ attribute :session
7
+ # attribute :user
8
+ end
9
+ end
@@ -0,0 +1,107 @@
1
+ require "chronic"
2
+ require "haml-rails"
3
+ require "kaminari"
4
+ require "image_processing"
5
+ require "propshaft"
6
+ require "pundit"
7
+ require "importmap-rails"
8
+ require "turbo-rails"
9
+ require "potter"
10
+ require "potter/type"
11
+
12
+ module CafeCar
13
+ class Engine < ::Rails::Engine
14
+ config.autoload_paths += paths["lib"].to_a
15
+
16
+ initializer "cafe_car.inflections" do
17
+ ActiveSupport::Inflector.inflections do |i|
18
+ i.acronym "UI"
19
+ end
20
+ end
21
+
22
+ initializer "cafe_car.i18n" do |app|
23
+ app.reloader.to_prepare do
24
+ CafeCar::NamePatch.patch!
25
+ I18n::Backend::Simple.include(CafeCar::Pluralization)
26
+ end
27
+ end
28
+
29
+ initializer "cafe_car.assets" do |app|
30
+ app.config.assets.paths << root.join("app/javascript")
31
+ end
32
+
33
+ initializer "cafe_car.preload_header" do |app|
34
+ app.config.action_view.preload_links_header = false
35
+ end
36
+
37
+ initializer "cafe_car.filter_parameters" do |app|
38
+ app.config.filter_parameters |= %i[authorization passw secret token _key crypt salt certificate otp ssn]
39
+ end
40
+
41
+ initializer "cafe_car.importmap", before: "importmap" do |app|
42
+ app.config.importmap.paths << root.join("config/importmap.rb")
43
+ app.config.importmap.cache_sweepers << root.join("app/javascript")
44
+ end
45
+
46
+ initializer "cafe_car.active_record" do |app|
47
+ app.reloader.to_prepare do
48
+ ::ActiveRecord::Base.include(Model)
49
+ end
50
+ end
51
+
52
+ initializer "cafe_car.routing" do |app|
53
+ app.reloader.to_prepare do
54
+ ::ActionDispatch::Routing::Mapper.include(Routing)
55
+ end
56
+ end
57
+
58
+ initializer "cafe_car.chronic" do |app|
59
+ app.reloader.to_prepare do
60
+ Chronic.time_class = Time.zone
61
+ end
62
+ end
63
+
64
+ initializer "cafe_car.sqlite_regexp" do |app|
65
+ ActiveSupport.on_load :active_record_sqlite3adapter do
66
+ ::Arel::Visitors::SQLite.include(Visitors::SQLite)
67
+ include ActiveRecord::SQLite3Extension
68
+ end
69
+ end
70
+
71
+ initializer "cafe_car.turbo" do
72
+ ActiveSupport.on_load :turbo_streams_tag_builder do
73
+ include TurboTagBuilder
74
+ end
75
+ end
76
+
77
+ initializer "cafe_car.responders" do
78
+ require "responders"
79
+ config.responders.flash_keys = [ :success, :error ]
80
+ end
81
+
82
+ initializer "cafe_car.field_with_errors" do
83
+ ActionView::Base.field_error_proc = proc { _1.html_safe }
84
+ end
85
+
86
+ initializer "cafe_car.console" do |app|
87
+ app.console do
88
+ TOPLEVEL_BINDING.eval("self").instance_exec do
89
+ def logger = Rails.logger
90
+
91
+ if defined?(FactoryBot)
92
+ include FactoryBot::Syntax::Methods
93
+ logger.info "FactoryBot methods enabled."
94
+ end
95
+
96
+ logger.info "SQL logs enabled."
97
+
98
+ ApplicationController.allow_forgery_protection = false
99
+ logger.info "CSRF disabled to enable app.post calls."
100
+
101
+ def present(...) = CafeCar[:Presenter].present(helper, ...)
102
+ def ui(...) = helper.ui(...)
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,44 @@
1
+ module CafeCar
2
+ class FieldBuilder
3
+ attr_reader :form, :method, :as
4
+
5
+ delegate :object, :info, to: :form
6
+
7
+ def initialize(method:, form:, template:, **options, &block)
8
+ @method = method
9
+ @form = form
10
+ @template = template
11
+ @options = options
12
+ @block = block
13
+ end
14
+
15
+ def info = form.info(@method)
16
+ def wrapper(...) = @template.ui.Field(...)
17
+ def input(...) = send_to_form(:input, ...)
18
+ def label(...) = send_to_form_with_text(:label, ...)
19
+ def hint(...) = send_to_form_with_text(:hint, ...)
20
+ def error(...) = send_to_form_with_text(:error, ...)
21
+
22
+
23
+ def html_safe? = true
24
+ def to_s
25
+ return input if info.polymorphic? and object.new_record?
26
+ partial = %W[#{info.type}_field field].find { @template.partial?(_1) }
27
+ @template.render(partial, field: self, **@options)
28
+ end
29
+
30
+ private
31
+
32
+ def add_class(*args, opts) = { class: @template.ui_class([ :field, *args ], *opts.delete(:class)), **opts }
33
+
34
+ def send_to_form_with_text(method, text = @options.delete(method), **, &)
35
+ return if text == false
36
+ send_to_form(method, *text, **, &)
37
+ end
38
+
39
+ def send_to_form(method, *, **opts, &)
40
+ return if @options[method] == false
41
+ form.public_send(method, @method, *, **add_class(*method, opts), &)
42
+ end
43
+ end
44
+ end