brut 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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"