brut 0.0.1

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 (131) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +7 -0
  3. data/CODE_OF_CONDUCT.txt +99 -0
  4. data/Dockerfile.dx +32 -0
  5. data/Gemfile +6 -0
  6. data/Gemfile.lock +133 -0
  7. data/LICENSE.txt +370 -0
  8. data/README.md +21 -0
  9. data/Rakefile +1 -0
  10. data/bin/bin_kit.rb +39 -0
  11. data/bin/rake +27 -0
  12. data/bin/setup +145 -0
  13. data/brut.gemspec +60 -0
  14. data/docker-compose.dx.yml +16 -0
  15. data/dx/build +26 -0
  16. data/dx/docker-compose.env +22 -0
  17. data/dx/dx.sh.lib +24 -0
  18. data/dx/exec +58 -0
  19. data/dx/prune +19 -0
  20. data/dx/setupkit.sh.lib +144 -0
  21. data/dx/show-help-in-app-container-then-wait.sh +38 -0
  22. data/dx/start +30 -0
  23. data/dx/stop +23 -0
  24. data/lib/brut/back_end/action.rb +3 -0
  25. data/lib/brut/back_end/result.rb +46 -0
  26. data/lib/brut/back_end/seed_data.rb +24 -0
  27. data/lib/brut/back_end/validator.rb +3 -0
  28. data/lib/brut/back_end/validators/form_validator.rb +37 -0
  29. data/lib/brut/cli/app.rb +130 -0
  30. data/lib/brut/cli/app_runner.rb +219 -0
  31. data/lib/brut/cli/apps/build_assets.rb +123 -0
  32. data/lib/brut/cli/apps/db.rb +279 -0
  33. data/lib/brut/cli/apps/scaffold.rb +256 -0
  34. data/lib/brut/cli/apps/test.rb +200 -0
  35. data/lib/brut/cli/command.rb +130 -0
  36. data/lib/brut/cli/error.rb +12 -0
  37. data/lib/brut/cli/execution_results.rb +81 -0
  38. data/lib/brut/cli/executor.rb +37 -0
  39. data/lib/brut/cli/options.rb +46 -0
  40. data/lib/brut/cli/output.rb +30 -0
  41. data/lib/brut/cli.rb +24 -0
  42. data/lib/brut/factory_bot.rb +20 -0
  43. data/lib/brut/framework/app.rb +55 -0
  44. data/lib/brut/framework/config.rb +415 -0
  45. data/lib/brut/framework/container.rb +190 -0
  46. data/lib/brut/framework/errors/abstract_method.rb +9 -0
  47. data/lib/brut/framework/errors/bug.rb +14 -0
  48. data/lib/brut/framework/errors/not_found.rb +10 -0
  49. data/lib/brut/framework/errors.rb +14 -0
  50. data/lib/brut/framework/fussy_type_enforcement.rb +50 -0
  51. data/lib/brut/framework/mcp.rb +215 -0
  52. data/lib/brut/framework/project_environment.rb +18 -0
  53. data/lib/brut/framework.rb +13 -0
  54. data/lib/brut/front_end/asset_metadata.rb +76 -0
  55. data/lib/brut/front_end/component.rb +213 -0
  56. data/lib/brut/front_end/components/form_tag.rb +71 -0
  57. data/lib/brut/front_end/components/i18n_translations.rb +36 -0
  58. data/lib/brut/front_end/components/input.rb +13 -0
  59. data/lib/brut/front_end/components/inputs/csrf_token.rb +8 -0
  60. data/lib/brut/front_end/components/inputs/select.rb +100 -0
  61. data/lib/brut/front_end/components/inputs/text_field.rb +63 -0
  62. data/lib/brut/front_end/components/inputs/textarea.rb +51 -0
  63. data/lib/brut/front_end/components/locale_detection.rb +25 -0
  64. data/lib/brut/front_end/components/page_identifier.rb +13 -0
  65. data/lib/brut/front_end/components/timestamp.rb +33 -0
  66. data/lib/brut/front_end/download.rb +23 -0
  67. data/lib/brut/front_end/flash.rb +57 -0
  68. data/lib/brut/front_end/form.rb +171 -0
  69. data/lib/brut/front_end/forms/constraint_violation.rb +39 -0
  70. data/lib/brut/front_end/forms/input.rb +119 -0
  71. data/lib/brut/front_end/forms/input_definition.rb +100 -0
  72. data/lib/brut/front_end/forms/validity_state.rb +36 -0
  73. data/lib/brut/front_end/handler.rb +48 -0
  74. data/lib/brut/front_end/handlers/csp_reporting_handler.rb +11 -0
  75. data/lib/brut/front_end/handlers/locale_detection_handler.rb +22 -0
  76. data/lib/brut/front_end/handling_results.rb +14 -0
  77. data/lib/brut/front_end/http_method.rb +33 -0
  78. data/lib/brut/front_end/http_status.rb +16 -0
  79. data/lib/brut/front_end/middleware.rb +7 -0
  80. data/lib/brut/front_end/middlewares/reload_app.rb +31 -0
  81. data/lib/brut/front_end/page.rb +47 -0
  82. data/lib/brut/front_end/request_context.rb +82 -0
  83. data/lib/brut/front_end/route_hook.rb +15 -0
  84. data/lib/brut/front_end/route_hooks/age_flash.rb +8 -0
  85. data/lib/brut/front_end/route_hooks/csp_no_inline_scripts.rb +17 -0
  86. data/lib/brut/front_end/route_hooks/csp_no_inline_styles_or_scripts.rb +46 -0
  87. data/lib/brut/front_end/route_hooks/locale_detection.rb +24 -0
  88. data/lib/brut/front_end/route_hooks/setup_request_context.rb +11 -0
  89. data/lib/brut/front_end/routing.rb +236 -0
  90. data/lib/brut/front_end/session.rb +56 -0
  91. data/lib/brut/front_end/template.rb +32 -0
  92. data/lib/brut/front_end/templates/block_filter.rb +60 -0
  93. data/lib/brut/front_end/templates/erb_engine.rb +26 -0
  94. data/lib/brut/front_end/templates/erb_parser.rb +84 -0
  95. data/lib/brut/front_end/templates/escapable_filter.rb +18 -0
  96. data/lib/brut/front_end/templates/html_safe_string.rb +40 -0
  97. data/lib/brut/i18n/base_methods.rb +168 -0
  98. data/lib/brut/i18n/for_cli.rb +4 -0
  99. data/lib/brut/i18n/for_html.rb +4 -0
  100. data/lib/brut/i18n/http_accept_language.rb +68 -0
  101. data/lib/brut/i18n.rb +6 -0
  102. data/lib/brut/instrumentation/basic.rb +66 -0
  103. data/lib/brut/instrumentation/event.rb +19 -0
  104. data/lib/brut/instrumentation/http_event.rb +5 -0
  105. data/lib/brut/instrumentation/subscriber.rb +41 -0
  106. data/lib/brut/instrumentation.rb +11 -0
  107. data/lib/brut/junk_drawer.rb +88 -0
  108. data/lib/brut/sinatra_helpers.rb +183 -0
  109. data/lib/brut/spec_support/component_support.rb +49 -0
  110. data/lib/brut/spec_support/flash_support.rb +7 -0
  111. data/lib/brut/spec_support/general_support.rb +18 -0
  112. data/lib/brut/spec_support/handler_support.rb +7 -0
  113. data/lib/brut/spec_support/matcher.rb +9 -0
  114. data/lib/brut/spec_support/matchers/be_a_bug.rb +14 -0
  115. data/lib/brut/spec_support/matchers/be_page_for.rb +14 -0
  116. data/lib/brut/spec_support/matchers/be_routing_for.rb +11 -0
  117. data/lib/brut/spec_support/matchers/have_constraint_violation.rb +56 -0
  118. data/lib/brut/spec_support/matchers/have_html_attribute.rb +69 -0
  119. data/lib/brut/spec_support/matchers/have_rendered.rb +20 -0
  120. data/lib/brut/spec_support/matchers/have_returned_http_status.rb +27 -0
  121. data/lib/brut/spec_support/session_support.rb +3 -0
  122. data/lib/brut/spec_support.rb +12 -0
  123. data/lib/brut/version.rb +3 -0
  124. data/lib/brut.rb +38 -0
  125. data/lib/sequel/extensions/brut_instrumentation.rb +37 -0
  126. data/lib/sequel/extensions/brut_migrations.rb +98 -0
  127. data/lib/sequel/plugins/created_at.rb +14 -0
  128. data/lib/sequel/plugins/external_id.rb +45 -0
  129. data/lib/sequel/plugins/find_bang.rb +13 -0
  130. data/lib/sequel/plugins.rb +3 -0
  131. metadata +484 -0
@@ -0,0 +1,49 @@
1
+ require_relative "flash_support"
2
+ module Brut::SpecSupport::ComponentSupport
3
+ include Brut::SpecSupport::FlashSupport
4
+ include Brut::I18n::ForHTML
5
+
6
+ def render_and_parse(component,&block)
7
+ if component.kind_of?(Brut::FrontEnd::Page)
8
+ if !block.nil?
9
+ raise "pages do not accept blocks - do not pass one to render_and_parse"
10
+ end
11
+ result = component.handle!
12
+ case result
13
+ in String => html
14
+ Nokogiri::HTML5(html)
15
+ else
16
+ result
17
+ end
18
+ else
19
+ component.yielded_block = block
20
+ rendered_text = component.render
21
+ document = Nokogiri::HTML5(rendered_text)
22
+ component_html = document.css("body")
23
+ if component_html
24
+ non_blank_text_elements = component_html.children.select { |element|
25
+ if element.kind_of?(Nokogiri::XML::Text) && element.text.to_s.strip == ""
26
+ false
27
+ else
28
+ true
29
+ end
30
+ }
31
+ if non_blank_text_elements.size == 1
32
+ non_blank_text_elements[0]
33
+ else
34
+ raise "#{component.class} rendered #{non_blank_text_elements.size} elements other than blank text:\n\n#{non_blank_text_elements.map(&:name)}. Components should render a single element:\n#{rendered_text}"
35
+ end
36
+ else
37
+ raise "#{component.class} did not render HTML properly: #{rendered_text}"
38
+ end
39
+ end
40
+ end
41
+
42
+ def routing_for(klass,**args)
43
+ Brut.container.routing.uri(klass,**args)
44
+ end
45
+
46
+ def escape_html(...)
47
+ Brut::FrontEnd::Templates::EscapableFilter.escape_html(...)
48
+ end
49
+ end
@@ -0,0 +1,7 @@
1
+ module Brut::SpecSupport::FlashSupport
2
+ def empty_flash = Brut.container.flash_class.new
3
+
4
+ def flash_from(hash)
5
+ Brut.container.flash_class.from_h(messages: hash)
6
+ end
7
+ end
@@ -0,0 +1,18 @@
1
+ module Brut::SpecSupport::GeneralSupport
2
+ def self.included(mod)
3
+ mod.extend(ClassMethods)
4
+ end
5
+
6
+ module ClassMethods
7
+ def implementation_is_trivial
8
+ it "has no tests because the implementation is trivial" do
9
+ expect(true).to eq(true)
10
+ end
11
+ end
12
+ def implementation_is_covered_by_other_tests(description)
13
+ it "has no tests because the implementation is sufficiently covered by other tests: #{description}" do
14
+ expect(true).to eq(true)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,7 @@
1
+ require_relative "flash_support"
2
+ require_relative "session_support"
3
+ module Brut::SpecSupport::HandlerSupport
4
+ include Brut::SpecSupport::FlashSupport
5
+ include Brut::SpecSupport::SessionSupport
6
+ end
7
+
@@ -0,0 +1,9 @@
1
+ module Brut::SpecSupport::Matchers
2
+ end
3
+ require_relative "matchers/have_constraint_violation"
4
+ require_relative "matchers/have_html_attribute"
5
+ require_relative "matchers/be_routing_for"
6
+ require_relative "matchers/be_page_for"
7
+ require_relative "matchers/have_rendered"
8
+ require_relative "matchers/have_returned_http_status"
9
+ require_relative "matchers/be_a_bug"
@@ -0,0 +1,14 @@
1
+ RSpec::Matchers.define :be_a_bug do
2
+ match(:notify_expectation_failures => true) do |actual|
3
+ exception = nil
4
+ begin
5
+ actual.call
6
+ rescue => ex
7
+ exception = ex
8
+ end
9
+ expect(exception).not_to eq(nil),"Expected a bug, but no exception was thrown"
10
+ expect(exception).to be_kind_of(Brut::Framework::Errors::Bug)
11
+ end
12
+
13
+ supports_block_expectations
14
+ end
@@ -0,0 +1,14 @@
1
+ RSpec::Matchers.define :be_page_for do |klass|
2
+ match do |page|
3
+ meta = page.locator("meta[name='class']")
4
+ expect(meta).to have_attribute("content", klass.name)
5
+ end
6
+ failure_message do |page|
7
+ meta = page.locator("meta[name='class']")
8
+ if meta.count == 0
9
+ "Could not find <meta name='class'> on the page:\n\n#{page.content}"
10
+ else
11
+ "Could not find <meta name='class' content='#{klass.name}'>, but found:\n\n#{meta.evaluate('e => e.outerHTML')}"
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,11 @@
1
+ RSpec::Matchers.define :be_routing_for do |klass,**args|
2
+ match do |uri|
3
+ uri == Brut.container.routing.uri(klass,**args)
4
+ end
5
+
6
+ failure_message do |uri|
7
+ expected = Brut.container.routing.uri(klass,**args)
8
+ "Expected route for #{klass}: #{expected}, but got #{uri}"
9
+ end
10
+
11
+ end
@@ -0,0 +1,56 @@
1
+ RSpec::Matchers.define :have_constraint_violation do |field,key:|
2
+ match do |form|
3
+ Brut::SpecSupport::Matchers::HaveConstraintViolation.new(form,field,key).matches?
4
+ end
5
+
6
+ failure_message do |form|
7
+ analysis = Brut::SpecSupport::Matchers::HaveConstraintViolation.new(form,field,key)
8
+ if analysis.found_field?
9
+ "#{field} did not have #{key} as a violation. These keys were found: #{analysis.keys_on_field_found.map(&:to_s).join(", ")}"
10
+ else
11
+ "#{field} had no errors. These fields DID: #{analysis.fields_found.map(&:to_s).join(", ")}"
12
+ end
13
+ end
14
+
15
+ failure_message_when_negated do |form|
16
+ "Found #{key} as a violation on #{field}"
17
+ end
18
+ end
19
+
20
+ class Brut::SpecSupport::Matchers::HaveConstraintViolation
21
+ attr_reader :fields_found
22
+ attr_reader :keys_on_field_found
23
+
24
+ def initialize(form, field, key)
25
+ if !form.kind_of?(Brut::FrontEnd::Form)
26
+ raise "#{self.class} only works with forms, not #{form.class}"
27
+ end
28
+ @form = form
29
+ @field = field.to_s
30
+ @key = key.to_s
31
+
32
+ @matches = false
33
+ @found_field = false
34
+ @fields_found = Set.new
35
+ @keys_on_field_found = Set.new
36
+
37
+ @form.constraint_violations.each do |input_name, constraint_violations|
38
+ if input_name.to_s == @field
39
+ @found_field = true
40
+ constraint_violations.each do |constraint_violation|
41
+ if constraint_violation.key.to_s == @key
42
+ @matches = true
43
+ else
44
+ @keys_on_field_found << constraint_violation.key.to_s
45
+ end
46
+ end
47
+ else
48
+ @fields_found << input_name.to_s
49
+ end
50
+ end
51
+ end
52
+
53
+ def matches? = @matches
54
+ def found_field? = @found_field
55
+
56
+ end
@@ -0,0 +1,69 @@
1
+ RSpec::Matchers.define :have_html_attribute do |attribute|
2
+ if attribute.kind_of?(Hash)
3
+ if attribute.keys.length != 1
4
+ raise "have_html_attribute requires a single hash with a single key, or a single symbol/string. Received #{attribute.keys.length} keys: '#{attribute.keys.map(&:to_s).join(", ")}'"
5
+ end
6
+ elsif !attribute.kind_of?(Symbol) && !attribute.kind_of?(String)
7
+ raise "have_html_attribute requires a single hash with a single key, or a single symbol/string. Received a #{attribute.class}"
8
+ end
9
+
10
+ match do |result|
11
+ Brut::SpecSupport::Matchers::HaveHTMLAttribute.new(result,attribute).matches?
12
+ end
13
+
14
+ failure_message do |result|
15
+ Brut::SpecSupport::Matchers::HaveHTMLAttribute.new(result,attribute).error
16
+ end
17
+
18
+ failure_message_when_negated do |result|
19
+ if attribute.kind_of?(Hash)
20
+ "Found attribute '#{attribute.keys.first}' with value '#{attribute.values.first}'"
21
+ else
22
+ "Found attribute '#{attribute}' when not expecting it #{result.to_html}"
23
+ end
24
+ end
25
+ end
26
+
27
+ class Brut::SpecSupport::Matchers::HaveHTMLAttribute
28
+
29
+ attr_reader :error
30
+
31
+ def initialize(result, attribute)
32
+ @error = nil
33
+ if result.kind_of?(Nokogiri::XML::NodeSet)
34
+ if result.length > 1
35
+ @error = "Received #{result.length} matching nodes, when only one should've been returned"
36
+ elsif result.length == 0
37
+ @error = "Received no matching nodes to examine"
38
+ else
39
+ result = result.first
40
+ end
41
+ elsif (!result.kind_of?(Nokogiri::XML::Element))
42
+ @error = "Received a #{result.class} instead of a NodeSet or Element, as could be returned by `.css(...)`"
43
+ end
44
+ if !@error
45
+ if attribute.kind_of?(Hash)
46
+ attribute_name = attribute.keys.first.to_s
47
+ attribute_value = attribute.values.first.to_s
48
+ else
49
+ attribute_name = attribute.to_s
50
+ attribute_value = :any
51
+ end
52
+
53
+ nokogiri_attribute = result.attribute(attribute_name)
54
+ if nokogiri_attribute
55
+ if attribute_value != :any
56
+ found_value = result.attribute(attribute_name).value
57
+
58
+ if found_value != attribute_value
59
+ @error = "Value for '#{attribute_name}' was '#{found_value}'. Expected '#{attribute_value}'"
60
+ end
61
+ end
62
+ else
63
+ @error = "Did not find attribute '#{attribute_name}' on element. Found: #{result.attributes.keys.join(", ")}"
64
+ end
65
+ end
66
+ end
67
+
68
+ def matches? = @error.nil?
69
+ end
@@ -0,0 +1,20 @@
1
+ RSpec::Matchers.define :have_rendered do
2
+ match do |result|
3
+ result.kind_of?(String) || result.kind_of?(Brut::FrontEnd::Templates::HTMLSafeString)
4
+ end
5
+
6
+ failure_message do |result|
7
+ case result
8
+ in URI => uri
9
+ "Got a redirect to #{uri} instead of rendering"
10
+ in Brut::FrontEnd::HttpStatus => http_status
11
+ "Got an HTTP status of #{http_status} instead of rendering"
12
+ else
13
+ "Got an unexpected result: #{result.class} instead of a String"
14
+ end
15
+ end
16
+ failure_message_when_negated do |result|
17
+ "Result was rendered HTML instead of something else"
18
+ end
19
+
20
+ end
@@ -0,0 +1,27 @@
1
+ RSpec::Matchers.define :have_returned_http_status do |http_status|
2
+ match do |result|
3
+ case result
4
+ in URI => uri
5
+ http_status == 302
6
+ in Brut::FrontEnd::HttpStatus => result_http_status
7
+ http_status == result_http_status.to_i
8
+ else
9
+ http_status == 200
10
+ end
11
+ end
12
+
13
+ failure_message do |result|
14
+ case result
15
+ in URI => uri
16
+ "Got a redirect (302) instead of a #{http_status}"
17
+ in Brut::FrontEnd::HttpStatus => result_http_status
18
+ "Got a #{result_http_status} instead of a #{http_status}"
19
+ else
20
+ "Got a render (200) instead of a #{http_status}"
21
+ end
22
+ end
23
+ failure_message_when_negated do |result|
24
+ "Result was rendered HTML instead of something else"
25
+ end
26
+
27
+ end
@@ -0,0 +1,3 @@
1
+ module Brut::SpecSupport::SessionSupport
2
+ def empty_session = Brut.container.session_class.new(rack_session: {})
3
+ end
@@ -0,0 +1,12 @@
1
+ # Convention here is different. We don't want to autoload
2
+ # a lot of stuff, since RSpec pollutes the Object namespace.
3
+ # Instead, we'll require that these files are required explicitly
4
+ module Brut
5
+ module SpecSupport
6
+ end
7
+ end
8
+ require_relative "spec_support/matcher"
9
+ require_relative "spec_support/component_support"
10
+ require_relative "spec_support/handler_support"
11
+ require_relative "spec_support/general_support"
12
+ require_relative "factory_bot"
@@ -0,0 +1,3 @@
1
+ module Brut
2
+ VERSION = "0.0.1"
3
+ end
data/lib/brut.rb ADDED
@@ -0,0 +1,38 @@
1
+ require_relative "brut/framework"
2
+
3
+ # Convention is as follows:
4
+ #
5
+ # * singluar thing is a class, the base class of others being used, e.g. the base page is called Brut::Page
6
+ # (e.g. and not Brut::Pages::Base).
7
+ module Brut
8
+ module FrontEnd
9
+ autoload(:AssetMetadata, "brut/front_end/asset_metadata")
10
+ autoload(:Component, "brut/front_end/component")
11
+ autoload(:Components, "brut/front_end/component")
12
+ autoload(:Download, "brut/front_end/download")
13
+ autoload(:Flash, "brut/front_end/flash")
14
+ autoload(:Form, "brut/front_end/form")
15
+ autoload(:Handler, "brut/front_end/handler")
16
+ autoload(:Handlers, "brut/front_end/handler")
17
+ autoload(:HandlingResults, "brut/front_end/handling_results")
18
+ autoload(:HttpMethod, "brut/front_end/http_method")
19
+ autoload(:HttpStatus, "brut/front_end/http_status")
20
+ autoload(:Middleware, "brut/front_end/middleware")
21
+ autoload(:Middlewares, "brut/front_end/middleware")
22
+ autoload(:Page, "brut/front_end/page")
23
+ autoload(:RequestContext, "brut/front_end/request_context")
24
+ autoload(:RouteHook, "brut/front_end/route_hook")
25
+ autoload(:RouteHooks, "brut/front_end/route_hook")
26
+ autoload(:Routing, "brut/front_end/routing")
27
+ autoload(:Session, "brut/front_end/session")
28
+ end
29
+ module BackEnd
30
+ autoload(:Result, "brut/back_end/result")
31
+ autoload(:Validators, "brut/back_end/validator")
32
+ end
33
+ # DO NOT autoload(:CLI) - that is intended to be require-able on its own
34
+ autoload(:I18n, "brut/i18n")
35
+ autoload(:Instrumentation,"brut/instrumentation")
36
+ autoload(:SinatraHelpers, "brut/sinatra_helpers")
37
+ end
38
+ require "sequel/plugins"
@@ -0,0 +1,37 @@
1
+ module Sequel
2
+ module Extensions
3
+ module BrutInstrumentation
4
+ class Event < Brut::Instrumentation::Event
5
+ def initialize(operation:,sql:nil)
6
+ super(category: "sequel", name: operation, details: { sql: sql })
7
+ end
8
+ end
9
+ def execute(sql, opts = Sequel::OPTS, &block)
10
+ Brut.container.instrumentation.instrument(Event.new(operation: "execute", sql: sql)) do
11
+ super
12
+ end
13
+ end
14
+ def execute_dui(sql, opts = Sequel::OPTS, &block)
15
+ Brut.container.instrumentation.instrument(Event.new(operation: "execute_dui", sql: sql)) do
16
+ super
17
+ end
18
+ end
19
+ def execute_insert(sql, opts = Sequel::OPTS, &block)
20
+ Brut.container.instrumentation.instrument(Event.new(operation: "execute_insert", sql: sql)) do
21
+ super
22
+ end
23
+ end
24
+ def insert_select(*values)
25
+ Brut.container.instrumentation.instrument(Event.new(operation: "insert_select", sql: values)) do
26
+ super
27
+ end
28
+ end
29
+ def returning_fetch_rows(sql,&block)
30
+ Brut.container.instrumentation.instrument(Event.new(operation: "returning_fetch_rows", sql: sql)) do
31
+ super
32
+ end
33
+ end
34
+ end
35
+ end
36
+ Sequel::Dataset.register_extension(:brut_instrumentation, Sequel::Extensions::BrutInstrumentation)
37
+ end
@@ -0,0 +1,98 @@
1
+ module Sequel
2
+ module Extensions
3
+ # Enhancements to migrations to encourage better practices and reduce boilerplate.
4
+ #
5
+ # * If no primary key is specified, a primary key column named :id of type :int will be created
6
+ # * If no created_at is specified, a column name created_at of type timestamptz is created
7
+ # * create_table requires a comment: field
8
+ # * create_table accepts an external_id: true option that will create a unique citext called "external_id"
9
+ # * columns are non-null by default
10
+ # * foreign keys are non-null and an index is created
11
+ # * the `key` method allows specifying keys aka creating a unique constraint
12
+ module BrutMigrations
13
+ def create_table(*args)
14
+ super
15
+
16
+ if args.last.is_a?(Hash)
17
+ if args.last[:comment]
18
+ run %{
19
+ comment on table #{args.first} is #{literal args.last[:comment]}
20
+ }
21
+ else
22
+ raise ArgumentError, "Table #{args.first} must have a comment"
23
+ end
24
+ if args.last[:external_id]
25
+ add_column args.first, :external_id, :citext, unique: true
26
+ end
27
+ end
28
+ end
29
+
30
+ def add_key(fields)
31
+ add_index fields, unique: true
32
+ end
33
+
34
+ def add_column(table,*args)
35
+ options = args.last
36
+ if options.is_a?(Hash)
37
+ if !options.key?(:null)
38
+ options[:null] = false
39
+ end
40
+ end
41
+ super(table,*args)
42
+ end
43
+
44
+ def create_table_from_generator(name, generator, options)
45
+ if name != "schema_migrations"
46
+ if generator.columns.none? { |column| column[:primary_key] }
47
+ generator.primary_key :id
48
+ end
49
+ if generator.columns.none? { |column| column[:name].to_s == "created_at" }
50
+ generator.column :created_at, :timestamptz, null: false
51
+ end
52
+ generator.columns.each do |column|
53
+ if !column.key?(:null)
54
+ column[:null] = false
55
+ end
56
+ if column.key?(:table)
57
+ if !column.key?(:index)
58
+ column[:index] = true
59
+ generator.index(column[:name])
60
+ end
61
+ end
62
+ end
63
+ end
64
+ super
65
+ end
66
+ end
67
+ end
68
+ end
69
+ Sequel::Database.register_extension(:brut_migrations) do |db|
70
+ db.extend Sequel::Extensions::BrutMigrations
71
+ class ::Sequel::Schema::CreateTableGenerator
72
+ def key(fields)
73
+ index fields, unique: true
74
+ end
75
+ end
76
+ class ::Sequel::Schema::AlterTableGenerator
77
+ def add_column_with_additions(name, type, opts={})
78
+ if !opts.key?(:null)
79
+ opts[:null] = false
80
+ end
81
+ add_column_base(name,type,opts)
82
+ end
83
+ alias_method :add_column_base, :add_column
84
+ alias_method :add_column, :add_column_with_additions
85
+
86
+ def add_foreign_key_with_additions(name, table, opts={})
87
+ if !opts.key?(:index)
88
+ opts[:index] = true
89
+ end
90
+ if !opts.key?(:null)
91
+ opts[:null] = false
92
+ end
93
+ add_foreign_key_base(name, table, opts)
94
+ end
95
+ alias_method :add_foreign_key_base, :add_foreign_key
96
+ alias_method :add_foreign_key, :add_foreign_key_with_additions
97
+ end
98
+ end
@@ -0,0 +1,14 @@
1
+ module Sequel
2
+ module Plugins
3
+ module CreatedAt
4
+ module InstanceMethods
5
+ def before_save
6
+ if self.created_at.nil?
7
+ self.created_at = Time.now
8
+ end
9
+ super
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,45 @@
1
+ module Sequel
2
+ module Plugins
3
+ module ExternalId
4
+ def self.apply(model,*args,&block)
5
+ @global_prefix = (args.first || {})[:global_prefix]
6
+ end
7
+
8
+ module ClassMethods
9
+ attr_reader :global_prefix
10
+ def has_external_id(prefix)
11
+ global_prefix = find_global_prefix
12
+ @external_id_prefix = RichString.new("#{global_prefix}#{prefix}").to_s_or_nil
13
+ end
14
+
15
+ def external_id_prefix = @external_id_prefix
16
+
17
+ def find_global_prefix(receiver=self)
18
+ if receiver.respond_to?(:global_prefix)
19
+ if receiver.global_prefix.nil?
20
+ receiver.ancestors.select { |ancestor| ancestor != receiver }.map { |ancestor|
21
+ self.find_global_prefix(ancestor)
22
+ }.compact.first
23
+ else
24
+ receiver.global_prefix
25
+ end
26
+ else
27
+ nil
28
+ end
29
+ end
30
+ end
31
+
32
+ module InstanceMethods
33
+ def before_save
34
+ if self.class.external_id_prefix
35
+ if self.external_id.nil?
36
+ random_hex = SecureRandom.hex
37
+ self.external_id = "#{self.class.external_id_prefix}_#{random_hex}"
38
+ end
39
+ end
40
+ super
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,13 @@
1
+ module Sequel
2
+ module Plugins
3
+ module FindBang
4
+ module ClassMethods
5
+ def find!(**args)
6
+ self.first!(**args)
7
+ rescue Sequel::NoMatchingRow => ex
8
+ raise Sequel::NoMatchingRow.new(ex.message + "; #{args.inspect}")
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,3 @@
1
+ require_relative "plugins/external_id"
2
+ require_relative "plugins/find_bang"
3
+ require_relative "plugins/created_at"