brut 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +7 -0
- data/CODE_OF_CONDUCT.txt +99 -0
- data/Dockerfile.dx +32 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +133 -0
- data/LICENSE.txt +370 -0
- data/README.md +21 -0
- data/Rakefile +1 -0
- data/bin/bin_kit.rb +39 -0
- data/bin/rake +27 -0
- data/bin/setup +145 -0
- data/brut.gemspec +60 -0
- data/docker-compose.dx.yml +16 -0
- data/dx/build +26 -0
- data/dx/docker-compose.env +22 -0
- data/dx/dx.sh.lib +24 -0
- data/dx/exec +58 -0
- data/dx/prune +19 -0
- data/dx/setupkit.sh.lib +144 -0
- data/dx/show-help-in-app-container-then-wait.sh +38 -0
- data/dx/start +30 -0
- data/dx/stop +23 -0
- data/lib/brut/back_end/action.rb +3 -0
- data/lib/brut/back_end/result.rb +46 -0
- data/lib/brut/back_end/seed_data.rb +24 -0
- data/lib/brut/back_end/validator.rb +3 -0
- data/lib/brut/back_end/validators/form_validator.rb +37 -0
- data/lib/brut/cli/app.rb +130 -0
- data/lib/brut/cli/app_runner.rb +219 -0
- data/lib/brut/cli/apps/build_assets.rb +123 -0
- data/lib/brut/cli/apps/db.rb +279 -0
- data/lib/brut/cli/apps/scaffold.rb +256 -0
- data/lib/brut/cli/apps/test.rb +200 -0
- data/lib/brut/cli/command.rb +130 -0
- data/lib/brut/cli/error.rb +12 -0
- data/lib/brut/cli/execution_results.rb +81 -0
- data/lib/brut/cli/executor.rb +37 -0
- data/lib/brut/cli/options.rb +46 -0
- data/lib/brut/cli/output.rb +30 -0
- data/lib/brut/cli.rb +24 -0
- data/lib/brut/factory_bot.rb +20 -0
- data/lib/brut/framework/app.rb +55 -0
- data/lib/brut/framework/config.rb +415 -0
- data/lib/brut/framework/container.rb +190 -0
- data/lib/brut/framework/errors/abstract_method.rb +9 -0
- data/lib/brut/framework/errors/bug.rb +14 -0
- data/lib/brut/framework/errors/not_found.rb +10 -0
- data/lib/brut/framework/errors.rb +14 -0
- data/lib/brut/framework/fussy_type_enforcement.rb +50 -0
- data/lib/brut/framework/mcp.rb +215 -0
- data/lib/brut/framework/project_environment.rb +18 -0
- data/lib/brut/framework.rb +13 -0
- data/lib/brut/front_end/asset_metadata.rb +76 -0
- data/lib/brut/front_end/component.rb +213 -0
- data/lib/brut/front_end/components/form_tag.rb +71 -0
- data/lib/brut/front_end/components/i18n_translations.rb +36 -0
- data/lib/brut/front_end/components/input.rb +13 -0
- data/lib/brut/front_end/components/inputs/csrf_token.rb +8 -0
- data/lib/brut/front_end/components/inputs/select.rb +100 -0
- data/lib/brut/front_end/components/inputs/text_field.rb +63 -0
- data/lib/brut/front_end/components/inputs/textarea.rb +51 -0
- data/lib/brut/front_end/components/locale_detection.rb +25 -0
- data/lib/brut/front_end/components/page_identifier.rb +13 -0
- data/lib/brut/front_end/components/timestamp.rb +33 -0
- data/lib/brut/front_end/download.rb +23 -0
- data/lib/brut/front_end/flash.rb +57 -0
- data/lib/brut/front_end/form.rb +171 -0
- data/lib/brut/front_end/forms/constraint_violation.rb +39 -0
- data/lib/brut/front_end/forms/input.rb +119 -0
- data/lib/brut/front_end/forms/input_definition.rb +100 -0
- data/lib/brut/front_end/forms/validity_state.rb +36 -0
- data/lib/brut/front_end/handler.rb +48 -0
- data/lib/brut/front_end/handlers/csp_reporting_handler.rb +11 -0
- data/lib/brut/front_end/handlers/locale_detection_handler.rb +22 -0
- data/lib/brut/front_end/handling_results.rb +14 -0
- data/lib/brut/front_end/http_method.rb +33 -0
- data/lib/brut/front_end/http_status.rb +16 -0
- data/lib/brut/front_end/middleware.rb +7 -0
- data/lib/brut/front_end/middlewares/reload_app.rb +31 -0
- data/lib/brut/front_end/page.rb +47 -0
- data/lib/brut/front_end/request_context.rb +82 -0
- data/lib/brut/front_end/route_hook.rb +15 -0
- data/lib/brut/front_end/route_hooks/age_flash.rb +8 -0
- data/lib/brut/front_end/route_hooks/csp_no_inline_scripts.rb +17 -0
- data/lib/brut/front_end/route_hooks/csp_no_inline_styles_or_scripts.rb +46 -0
- data/lib/brut/front_end/route_hooks/locale_detection.rb +24 -0
- data/lib/brut/front_end/route_hooks/setup_request_context.rb +11 -0
- data/lib/brut/front_end/routing.rb +236 -0
- data/lib/brut/front_end/session.rb +56 -0
- data/lib/brut/front_end/template.rb +32 -0
- data/lib/brut/front_end/templates/block_filter.rb +60 -0
- data/lib/brut/front_end/templates/erb_engine.rb +26 -0
- data/lib/brut/front_end/templates/erb_parser.rb +84 -0
- data/lib/brut/front_end/templates/escapable_filter.rb +18 -0
- data/lib/brut/front_end/templates/html_safe_string.rb +40 -0
- data/lib/brut/i18n/base_methods.rb +168 -0
- data/lib/brut/i18n/for_cli.rb +4 -0
- data/lib/brut/i18n/for_html.rb +4 -0
- data/lib/brut/i18n/http_accept_language.rb +68 -0
- data/lib/brut/i18n.rb +6 -0
- data/lib/brut/instrumentation/basic.rb +66 -0
- data/lib/brut/instrumentation/event.rb +19 -0
- data/lib/brut/instrumentation/http_event.rb +5 -0
- data/lib/brut/instrumentation/subscriber.rb +41 -0
- data/lib/brut/instrumentation.rb +11 -0
- data/lib/brut/junk_drawer.rb +88 -0
- data/lib/brut/sinatra_helpers.rb +183 -0
- data/lib/brut/spec_support/component_support.rb +49 -0
- data/lib/brut/spec_support/flash_support.rb +7 -0
- data/lib/brut/spec_support/general_support.rb +18 -0
- data/lib/brut/spec_support/handler_support.rb +7 -0
- data/lib/brut/spec_support/matcher.rb +9 -0
- data/lib/brut/spec_support/matchers/be_a_bug.rb +14 -0
- data/lib/brut/spec_support/matchers/be_page_for.rb +14 -0
- data/lib/brut/spec_support/matchers/be_routing_for.rb +11 -0
- data/lib/brut/spec_support/matchers/have_constraint_violation.rb +56 -0
- data/lib/brut/spec_support/matchers/have_html_attribute.rb +69 -0
- data/lib/brut/spec_support/matchers/have_rendered.rb +20 -0
- data/lib/brut/spec_support/matchers/have_returned_http_status.rb +27 -0
- data/lib/brut/spec_support/session_support.rb +3 -0
- data/lib/brut/spec_support.rb +12 -0
- data/lib/brut/version.rb +3 -0
- data/lib/brut.rb +38 -0
- data/lib/sequel/extensions/brut_instrumentation.rb +37 -0
- data/lib/sequel/extensions/brut_migrations.rb +98 -0
- data/lib/sequel/plugins/created_at.rb +14 -0
- data/lib/sequel/plugins/external_id.rb +45 -0
- data/lib/sequel/plugins/find_bang.rb +13 -0
- data/lib/sequel/plugins.rb +3 -0
- 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,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,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,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"
|
data/lib/brut/version.rb
ADDED
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,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
|