Skip to content

Commit 86b7945

Browse files
authored
Merge pull request #1 from code0-tech/378-extract-common-helpers-from-sagittarius
Extract common helpers from sagittarius
2 parents 5e23ec6 + c77f240 commit 86b7945

33 files changed

+1588
-7
lines changed

Gemfile.lock

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ PATH
33
specs:
44
code0-zero_track (0.0.0)
55
rails (>= 8.0.1)
6+
zeitwerk (~> 2.7)
67

78
GEM
89
remote: https://rubygems.org/
@@ -82,11 +83,14 @@ GEM
8283
base64 (0.2.0)
8384
benchmark (0.4.0)
8485
bigdecimal (3.1.9)
86+
binding_of_caller (1.0.1)
87+
debug_inspector (>= 1.2.0)
8588
builder (3.3.0)
8689
concurrent-ruby (1.3.5)
8790
connection_pool (2.5.0)
8891
crass (1.0.6)
8992
date (3.4.1)
93+
debug_inspector (1.2.0)
9094
diff-lcs (1.6.0)
9195
drb (2.2.1)
9296
erubi (1.13.1)
@@ -147,6 +151,10 @@ GEM
147151
pp (0.6.2)
148152
prettyprint
149153
prettyprint (0.2.0)
154+
proc_to_ast (0.2.0)
155+
parser
156+
rouge
157+
unparser
150158
psych (5.2.3)
151159
date
152160
stringio
@@ -195,6 +203,7 @@ GEM
195203
regexp_parser (2.10.0)
196204
reline (0.6.0)
197205
io-console (~> 0.5)
206+
rouge (4.5.1)
198207
rspec (3.13.0)
199208
rspec-core (~> 3.13.0)
200209
rspec-expectations (~> 3.13.0)
@@ -207,6 +216,17 @@ GEM
207216
rspec-mocks (3.13.2)
208217
diff-lcs (>= 1.2.0, < 2.0)
209218
rspec-support (~> 3.13.0)
219+
rspec-parameterized (1.0.2)
220+
rspec-parameterized-core (< 2)
221+
rspec-parameterized-table_syntax (< 2)
222+
rspec-parameterized-core (1.0.1)
223+
parser
224+
proc_to_ast (>= 0.2.0)
225+
rspec (>= 2.13, < 4)
226+
unparser
227+
rspec-parameterized-table_syntax (1.0.1)
228+
binding_of_caller
229+
rspec-parameterized-core (< 2)
210230
rspec-rails (7.1.1)
211231
actionpack (>= 7.0)
212232
activesupport (>= 7.0)
@@ -253,6 +273,9 @@ GEM
253273
unicode-display_width (3.1.4)
254274
unicode-emoji (~> 4.0, >= 4.0.4)
255275
unicode-emoji (4.0.4)
276+
unparser (0.6.15)
277+
diff-lcs (~> 1.3)
278+
parser (>= 3.3.0)
256279
uri (1.0.2)
257280
useragent (0.16.11)
258281
websocket-driver (0.7.7)
@@ -275,6 +298,7 @@ DEPENDENCIES
275298
code0-zero_track!
276299
rake (~> 13.0)
277300
rspec (~> 3.0)
301+
rspec-parameterized (~> 1.0)
278302
rspec-rails (~> 7.0)
279303
rubocop-rails (~> 2.19)
280304
rubocop-rake (~> 0.7.0)

bin/console

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
require 'bundler/setup'
5+
require 'code0/zero_track'
6+
7+
# You can add fixtures and/or initialization code here to make experimenting
8+
# with your gem easier. You can also use a different console, if you like.
9+
require 'irb'
10+
ENV['IRB_USE_AUTOCOMPLETE'] = 'false'
11+
IRB.start(__FILE__)

code0-zero_track.gemspec

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,21 @@ Gem::Specification.new do |spec|
1515
spec.metadata['homepage_uri'] = spec.homepage
1616
spec.metadata['source_code_uri'] = spec.homepage
1717
spec.metadata['changelog_uri'] = "#{spec.homepage}/releases"
18+
spec.metadata['rubygems_mfa_required'] = 'true'
1819

1920
spec.files = Dir.chdir(File.expand_path(__dir__)) do
2021
Dir['{app,config,db,lib}/**/*', 'LICENSE', 'Rakefile', 'README.md']
2122
end
2223

2324
spec.add_dependency 'rails', '>= 8.0.1'
25+
spec.add_dependency 'zeitwerk', '~> 2.7'
2426

2527
spec.add_development_dependency 'rake', '~> 13.0'
2628
spec.add_development_dependency 'rspec', '~> 3.0'
29+
spec.add_development_dependency 'rspec-parameterized', '~> 1.0'
2730
spec.add_development_dependency 'rspec-rails', '~> 7.0'
2831
spec.add_development_dependency 'rubocop-rails', '~> 2.19'
2932
spec.add_development_dependency 'rubocop-rake', '~> 0.7.0'
3033
spec.add_development_dependency 'rubocop-rspec', '~> 3.0'
3134
spec.add_development_dependency 'rubocop-rspec_rails', '~> 2.30'
32-
spec.metadata['rubygems_mfa_required'] = 'true'
3335
end

lib/code0/zero_track.rb

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
# frozen_string_literal: true
22

3-
require 'code0/zero_track/version'
4-
require 'code0/zero_track/railtie'
3+
require 'rails/railtie'
4+
5+
require 'zeitwerk'
6+
loader = Zeitwerk::Loader.new
7+
loader.tag = File.basename(__FILE__, '.rb')
8+
loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
9+
loader.push_dir(File.expand_path(File.join(__dir__, '..')))
10+
loader.setup
511

612
module Code0
713
module ZeroTrack
814
# Your code goes here...
915
end
1016
end
17+
18+
Code0::ZeroTrack::Railtie # eager load the railtie

lib/code0/zero_track/context.rb

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# frozen_string_literal: true
2+
3+
module Code0
4+
module ZeroTrack
5+
class Context
6+
LOG_KEY = 'meta'
7+
CORRELATION_ID_KEY = 'correlation_id'
8+
RAW_KEYS = [CORRELATION_ID_KEY].freeze
9+
10+
class << self
11+
def with_context(attributes = {})
12+
context = push(attributes)
13+
14+
begin
15+
yield(context)
16+
ensure
17+
pop(context)
18+
end
19+
end
20+
21+
def push(new_attributes = {})
22+
new_context = current&.merge(new_attributes) || new(new_attributes)
23+
24+
contexts.push(new_context)
25+
26+
new_context
27+
end
28+
29+
def pop(context)
30+
contexts.pop while contexts.include?(context)
31+
end
32+
33+
def correlation_id
34+
current&.correlation_id
35+
end
36+
37+
def current
38+
contexts.last
39+
end
40+
41+
def log_key(key)
42+
key = key.to_s
43+
return key if RAW_KEYS.include?(key)
44+
return key if key.start_with?("#{LOG_KEY}.")
45+
46+
"#{LOG_KEY}.#{key}"
47+
end
48+
49+
private
50+
51+
def contexts
52+
Thread.current[:labkit_contexts] ||= []
53+
end
54+
end
55+
56+
def initialize(values = {})
57+
@data = {}
58+
59+
assign_attributes(values)
60+
end
61+
62+
def merge(new_attributes)
63+
new_context = self.class.new(data.dup)
64+
new_context.assign_attributes(new_attributes)
65+
66+
new_context
67+
end
68+
69+
def to_h
70+
expand_data
71+
end
72+
73+
def [](key)
74+
to_h[log_key(key)]
75+
end
76+
77+
def correlation_id
78+
data[CORRELATION_ID_KEY]
79+
end
80+
81+
def get_attribute(attribute)
82+
raw = call_or_value(data[log_key(attribute)])
83+
84+
call_or_value(raw)
85+
end
86+
87+
protected
88+
89+
def assign_attributes(attributes)
90+
attributes = attributes.transform_keys(&method(:log_key))
91+
92+
data.merge!(attributes)
93+
94+
# Remove keys that had their values set to `nil` in the new attributes
95+
data.keep_if { |_, value| valid_data?(value) }
96+
97+
# Assign a correlation if it was missing in the first context or when
98+
# explicitly removed
99+
data[CORRELATION_ID_KEY] ||= new_id
100+
101+
data
102+
end
103+
104+
private
105+
106+
attr_reader :data
107+
108+
def log_key(key)
109+
self.class.log_key(key)
110+
end
111+
112+
def call_or_value(value)
113+
value.respond_to?(:call) ? value.call : value
114+
end
115+
116+
def expand_data
117+
data.transform_values do |value|
118+
value = call_or_value(value)
119+
120+
value if valid_data?(value)
121+
end.compact
122+
end
123+
124+
def new_id
125+
SecureRandom.hex
126+
end
127+
128+
def valid_data?(value)
129+
value == false || value.present?
130+
end
131+
end
132+
end
133+
end
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# frozen_string_literal: true
2+
3+
module Code0
4+
module ZeroTrack
5+
module Database
6+
module ColumnMethods
7+
module Timestamps
8+
# Appends columns `created_at` and `updated_at` to a table.
9+
#
10+
# It is used in table creation like:
11+
# create_table 'users' do |t|
12+
# t.timestamps_with_timezone
13+
# end
14+
def timestamps_with_timezone(**options)
15+
options[:null] = false if options[:null].nil?
16+
17+
%i[created_at updated_at].each do |column_name|
18+
column(column_name, :datetime_with_timezone, **options)
19+
end
20+
end
21+
22+
# Adds specified column with appropriate timestamp type
23+
#
24+
# It is used in table creation like:
25+
# create_table 'users' do |t|
26+
# t.datetime_with_timezone :did_something_at
27+
# end
28+
def datetime_with_timezone(column_name, **options)
29+
column(column_name, :datetime_with_timezone, **options)
30+
end
31+
end
32+
end
33+
end
34+
end
35+
end
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# frozen_string_literal: true
2+
3+
module Code0
4+
module ZeroTrack
5+
module Database
6+
class Migration
7+
# rubocop:disable Naming/ClassAndModuleCamelCase
8+
class V1_0 < ::ActiveRecord::Migration[7.1]
9+
include Database::MigrationHelpers::AddColumnEnhancements
10+
include Database::MigrationHelpers::ConstraintHelpers
11+
include Database::MigrationHelpers::IndexHelpers
12+
include Database::MigrationHelpers::TableEnhancements
13+
end
14+
# rubocop:enable Naming/ClassAndModuleCamelCase
15+
16+
def self.[](version)
17+
version = version.to_s
18+
name = "V#{version.tr('.', '_')}"
19+
raise ArgumentError, "Invalid migration version: #{version}" unless const_defined?(name, false)
20+
21+
const_get(name, false)
22+
end
23+
end
24+
end
25+
end
26+
end
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# frozen_string_literal: true
2+
3+
module Code0
4+
module ZeroTrack
5+
module Database
6+
module MigrationHelpers
7+
module AddColumnEnhancements
8+
def add_column(table_name, column_name, type, *args, **kwargs, &block)
9+
helper_context = self
10+
11+
limit = kwargs.delete(:limit)
12+
unique = kwargs.delete(:unique)
13+
14+
super
15+
16+
return unless type == :text
17+
18+
quoted_column_name = helper_context.quote_column_name(column_name)
19+
20+
if limit
21+
name = helper_context.send(:text_limit_name, table_name, column_name)
22+
23+
definition = "char_length(#{quoted_column_name}) <= #{limit}"
24+
25+
add_check_constraint(table_name, definition, name: name)
26+
end
27+
28+
if unique.is_a?(Hash)
29+
unique[:where] = "#{column_name} IS NOT NULL" if unique.delete(:allow_nil_duplicate)
30+
column_name = "LOWER(#{quoted_column_name})" if unique.delete(:case_insensitive)
31+
32+
add_index table_name, column_name, unique: true, **unique
33+
elsif unique
34+
add_index table_name, column_name, unique: unique
35+
end
36+
end
37+
end
38+
end
39+
end
40+
end
41+
end
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# frozen_string_literal: true
2+
3+
module Code0
4+
module ZeroTrack
5+
module Database
6+
module MigrationHelpers
7+
module ConstraintHelpers
8+
def text_limit_name(table, column, name: nil)
9+
name.presence || check_constraint_name(table, column, 'max_length')
10+
end
11+
12+
def check_constraint_name(table, column, type)
13+
identifier = "#{table}_#{column}_check_#{type}"
14+
hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10)
15+
16+
"check_#{hashed_identifier}"
17+
end
18+
end
19+
end
20+
end
21+
end
22+
end

0 commit comments

Comments
 (0)