arql 0.3.31 → 0.4.0

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.
@@ -5,7 +5,7 @@
5
5
  创建一个文件 =~/.arql.d/auto_gen_id.rb= ,内容如下:
6
6
 
7
7
  #+BEGIN_SRC ruby
8
- class ::ArqlModel
8
+ class ::Arql::BaseModel
9
9
  before_create do
10
10
  if id.blank?
11
11
  id_type = self.class.columns_hash['id'].sql_type.scan(/\w+/).first
@@ -1,6 +1,6 @@
1
1
  * 配置文件中的自定义配置项
2
2
 
3
- 你可以在配置文件 (如默认的 =~/.arql.yaml= / =~/.arql.d/init.yaml= ) 中定义自己的配置项,然后在代码中通过 =Arql::App.config["CONF_KEY"]-= 来获取配置项的值。
3
+ 你可以在配置文件 (如默认的 =~/.arql.yaml= / =~/.arql.d/init.yaml= ) 中定义自己的配置项,然后在代码中通过 =env_config(/my_env/)["CONF_KEY"]-= 来获取配置项的值。
4
4
 
5
5
  例如,假设系统对 BankAccount 表的 =account_no= 字段进行了加密,你可以在配置文件中定义加密的密钥:
6
6
 
@@ -23,7 +23,7 @@
23
23
  def self.encrypt_account_no(account_no)
24
24
  cipher = OpenSSL::Cipher.new('AES-128-ECB')
25
25
  cipher.encrypt
26
- cipher.key = Arql::App.config["encrypt_key"]
26
+ cipher.key = env_config(/my_env/)["encrypt_key"]
27
27
  encrypted = cipher.update(account_no) + cipher.final
28
28
  encrypted.unpack('H*').first
29
29
  end
@@ -31,7 +31,7 @@
31
31
  def self.decrypt_account_no(encrypted_account_no)
32
32
  cipher = OpenSSL::Cipher.new('AES-128-ECB')
33
33
  cipher.decrypt
34
- cipher.key = Arql::App.config["encrypt_key"]
34
+ cipher.key = env_config(/my_env/)["encrypt_key"]
35
35
  decrypted = cipher.update([encrypted_account_no].pack('H*')) + cipher.final
36
36
  decrypted
37
37
  end
@@ -48,3 +48,40 @@
48
48
  end
49
49
  end
50
50
  #+END_SRC
51
+
52
+ 也可以直接使用 Namespace Module 的 config 方法来获取配置项的值,例如:
53
+
54
+ 假设 Namespace Module 为 =NS=, 那么上述代码可以改写为:
55
+
56
+ #+BEGIN_SRC ruby
57
+ class BankAccount
58
+
59
+ def self.encrypt_account_no(account_no)
60
+ cipher = OpenSSL::Cipher.new('AES-128-ECB')
61
+ cipher.encrypt
62
+ cipher.key = NS::config["encrypt_key"]
63
+ encrypted = cipher.update(account_no) + cipher.final
64
+ encrypted.unpack('H*').first
65
+ end
66
+
67
+ def self.decrypt_account_no(encrypted_account_no)
68
+ cipher = OpenSSL::Cipher.new('AES-128-ECB')
69
+ cipher.decrypt
70
+ cipher.key = NS::config["encrypt_key"]
71
+ decrypted = cipher.update([encrypted_account_no].pack('H*')) + cipher.final
72
+ decrypted
73
+ end
74
+
75
+
76
+ # 从数据库查询出数据之后,自动解密 account_no 字段
77
+ after_find do
78
+ self.password = decrypt_account_no(self.password)
79
+ end
80
+
81
+ # 保存数据之前,自动加密 account_no 字段
82
+ before_save do
83
+ self.password = encrypt_account_no(self.password)
84
+ end
85
+ end
86
+ #+END_SRC
87
+
@@ -5,26 +5,29 @@
5
5
  Initializer 文件是一个 Ruby 文件,因此可以在其中定义关联关系,例如:
6
6
 
7
7
  #+BEGIN_SRC ruby
8
- class Student
9
- has_many :courses, foreign_key: :student_id, class_name: 'Course'
10
- belongs_to :school, foreign_key: :school_id, class_name: 'School'
11
-
12
- has_and_belongs_to_many :teachers, join_table: 'students_teachers', foreign_key: :student_id, association_foreign_key: :teacher_id, class_name: 'Teacher'
13
- end
14
-
15
- class Course
16
- belongs_to :student, foreign_key: :student_id, class_name: 'Student'
17
- end
18
-
19
- class School
20
- has_many :students, foreign_key: :school_id, class_name: 'Student'
21
- end
22
-
23
- class Teacher
24
- has_and_belongs_to_many :students, join_table: 'students_teachers', foreign_key: :teacher_id, association_foreign_key: :student_id, class_name: 'Student'
8
+ module Blog
9
+ class Student
10
+ has_many :courses, foreign_key: :student_id, class_name: 'Course'
11
+ belongs_to :school, foreign_key: :school_id, class_name: 'School'
12
+
13
+ has_and_belongs_to_many :teachers, join_table: 'students_teachers', foreign_key: :student_id, association_foreign_key: :teacher_id, class_name: 'Teacher'
14
+ end
15
+
16
+ class Course
17
+ belongs_to :student, foreign_key: :student_id, class_name: 'Student'
18
+ end
19
+
20
+ class School
21
+ has_many :students, foreign_key: :school_id, class_name: 'Student'
22
+ end
23
+
24
+ class Teacher
25
+ has_and_belongs_to_many :students, join_table: 'students_teachers', foreign_key: :teacher_id, association_foreign_key: :student_id, class_name: 'Student'
26
+ end
25
27
  end
26
28
  #+END_SRC
27
29
 
30
+
28
31
  1. =has_one= 表明此表是一对一关系的属主
29
32
  2. =belongs_to= 表明此表是一对多或一对一关系的从属方
30
33
  3. =has_and_belongs_to_many= 表明此表是多对多关系的其中一方
@@ -36,3 +39,14 @@
36
39
 
37
40
  可以参考: https://guides.rubyonrails.org/association_basics.html
38
41
 
42
+ 考虑到模型类都是定义在 Namespace module 下面的, 因此这里的 Blog 是必要的。
43
+
44
+ 当然,不管通过 =-e= 选项选择了哪个环境,Arql 默认都会加载 =~/.arql.rb= 或 =~/.arql.d/init.rb= 文件,
45
+ 因此像上述示例中把固定的 Namespace =Blog= 放在默认的初始化文件中, 不是一个好的选择。
46
+
47
+ 有两种方案解决这个问题:
48
+
49
+ 1. 使用 arql 时,对于不同的环境,用 =-i= 选项来指定不同的初始化文件,例如: =arql -e blog -i ~/.arql.d/blog.rb=
50
+ 2. 参考 [[./initializer-structure-zh_CN.org][将不同环境的初始化代码放在不同的文件中]]
51
+
52
+
@@ -19,10 +19,11 @@
19
19
  然后在 =~/.arql.d/init.eb= 文件中写入以下代码:
20
20
 
21
21
  #+BEGIN_SRC ruby
22
- ["apollo", "space"].each do |project|
23
- if Arql::App.env.try { |e| e.include?(project + ".") }
24
- load(File.absolute_path(File.dirname(__FILE__) + "/#{project}.rb"))
25
- break
22
+ Dir.glob(File.dirname(__FILE__) + '/*.rb').each do |f|
23
+ Arql::App.instance.definitions.each do |env, definition|
24
+ if env.starts_with?(File.basename(f, '.rb'))
25
+ load(f, definition.namespace_module)
26
+ end
26
27
  end
27
28
  end
28
29
  #+END_SRC
@@ -30,4 +31,21 @@
30
31
  这样,当执行 =arql -e apollo.dev= 或 =arql =e apollo.prod= 时,就会加载 =apollo.rb= 文件中的初始化代码;当执行 =arql
31
32
  -e space.dev= 或 =arql -e space.prod= 时,就会加载 =space.rb= 文件中的初始化代码。
32
33
 
34
+ =apollo.rb= 或 =space.rb= 文件中的代码将在对应的 Namespace Module 下执行:
35
+
36
+ #+BEGIN_SRC ruby
37
+ class Astronaut
38
+ has_many :missions
39
+ end
40
+ #+END_SRC
41
+
42
+ 等价于:
43
+
44
+ #+BEGIN_SRC ruby
45
+ module Apollo
46
+ class Astronaut
47
+ has_many :missions
48
+ end
49
+ end
50
+ #+END_SRC
33
51
 
data/lib/arql/app.rb CHANGED
@@ -1,114 +1,141 @@
1
1
  module Arql
2
2
  class App
3
+ attr_accessor :log_io, :environments, :definitions, :options, :config
3
4
 
4
5
  class << self
5
- attr_accessor :log_io, :env, :prompt, :instance, :connect_options
6
+ attr_accessor :instance
6
7
 
7
- def config
8
- @@effective_config
8
+ def log_io
9
+ instance.log_io
10
+ end
11
+
12
+ def log_io=(io)
13
+ instance.log_io = io
14
+ end
15
+
16
+ # environment names
17
+ def environments
18
+ instance.environments
9
19
  end
10
20
 
11
21
  def prompt
12
- if env
13
- env
14
- else
15
- File.basename(@@effective_config[:database])
16
- end
22
+ instance.prompt
23
+ end
24
+
25
+ def config
26
+ instance.config
27
+ end
28
+ end
29
+
30
+ def prompt
31
+ if environments.present?
32
+ environments.join('+')
33
+ else
34
+ File.basename(@options.database)
17
35
  end
18
36
  end
19
37
 
20
38
  def initialize(options)
21
- require "arql/connection"
22
39
  require "arql/definition"
40
+
41
+ App.instance = self
42
+
43
+ # command line options
23
44
  @options = options
24
- App.env = @options.env
25
- App.connect_options = connect_options
26
- Connection.open(App.connect_options)
45
+
46
+ # env names
47
+ @environments = @options.environments
48
+ @environments ||= ['default']
49
+
27
50
  print "Defining models..."
28
- @definition = Definition.new(effective_config)
51
+ @definitions = config[:environments].each_with_object({}) do |(env_name, env_conf), h|
52
+ h[env_name] = Definition.new(env_conf)
53
+ end.with_indifferent_access
54
+
29
55
  print "\u001b[2K"
30
56
  puts "\rModels defined"
31
57
  print "Running initializers..."
32
58
  load_initializer!
33
59
  print "\u001b[2K"
34
60
  puts "\rInitializers loaded"
35
- App.instance = self
36
- end
37
-
38
- def connect_options
39
- connect_conf = effective_config.slice(:adapter, :host, :username,
40
- :password, :database, :encoding,
41
- :pool, :port, :socket)
42
- if effective_config[:ssh].present?
43
- connect_conf.merge!(start_ssh_proxy!)
44
- end
45
-
46
- connect_conf
47
61
  end
48
62
 
49
63
  def load_initializer!
50
- return unless effective_config[:initializer]
51
- initializer_file = File.expand_path(effective_config[:initializer])
64
+ return unless config[:options][:initializer]
65
+
66
+ initializer_file = File.expand_path(config[:options][:initializer])
52
67
  unless File.exist?(initializer_file)
53
- STDERR.puts "Specified initializer file not found, #{effective_config[:initializer]}"
68
+ warn "Specified initializer file not found, #{config[:options][:initializer]}"
54
69
  exit(1)
55
70
  end
56
71
  load(initializer_file)
57
72
  end
58
73
 
59
- def start_ssh_proxy!
60
- ssh_config = effective_config[:ssh]
61
- local_ssh_proxy_port = Arql::SSHProxy.connect(ssh_config.slice(:host, :user, :port, :password).merge(
62
- forward_host: effective_config[:host],
63
- forward_port: effective_config[:port],
64
- local_port: ssh_config[:local_port]))
65
- {
66
- host: '127.0.0.1',
67
- port: local_ssh_proxy_port
68
- }
69
- end
70
-
71
- def config
72
- @config ||= YAML.load(IO.read(File.expand_path(@options.config_file)), aliases: true).with_indifferent_access
74
+ def config_from_file
75
+ @config_from_file ||= YAML.safe_load(IO.read(File.expand_path(@options.config_file)), aliases: true).with_indifferent_access
73
76
  rescue ArgumentError
74
- @config ||= YAML.load(IO.read(File.expand_path(@options.config_file))).with_indifferent_access
77
+ @config_from_file ||= YAML.safe_load(IO.read(File.expand_path(@options.config_file))).with_indifferent_access
75
78
  end
76
79
 
77
- def selected_config
78
- if @options.env.present? && !config[@options.env].present?
79
- STDERR.puts "Specified ENV `#{@options.env}' not exists"
80
+ # Returns the configuration for config file.
81
+ # or default configuration (built from CLI options) if no environment specified
82
+ def environ_config_from_file
83
+ if @options.enviroments.present? || @options.environments&.any? { |env_names| !config_from_file.key?(env_names) }
84
+ warn "Specified ENV `#{@options.env}' not exists in config file"
85
+ exit(1)
80
86
  end
81
- if env = @options.env
82
- config[env]
87
+ conf = if @options.environments.present?
88
+ @config_from_file.slice(*@options.environments)
83
89
  else
84
- {}
90
+ { default: @options.to_h }.with_indifferent_access
85
91
  end
86
- end
87
-
88
- def effective_config
89
- @@effective_config ||= nil
90
- unless @@effective_config
91
- @@effective_config = selected_config.deep_merge(@options.to_h)
92
- if @@effective_config[:adapter].blank?
93
- @@effective_config[:adapter] = 'sqlite3'
92
+ conf.each do |env_name, env_conf|
93
+ unless env_conf.key?(:namespace)
94
+ env_conf[:namespace] = env_name.to_s.gsub(/[^a-zA-Z0-9]/, '_').camelize
94
95
  end
95
- @@effective_config[:database] = File.expand_path(@@effective_config[:database]) if @@effective_config[:adapter] == 'sqlite3'
96
96
  end
97
- @@effective_config
97
+ end
98
+
99
+ # Returns the effective configuration for the application.
100
+ # structure like:
101
+ # {
102
+ # options: {show_sql: true,
103
+ # write_sql: 'output.sql',
104
+ # },
105
+ # environments: {
106
+ # development: {adapter: 'mysql2',
107
+ # host: 'localhost',
108
+ # port: 3306},
109
+ # test: {adapter: 'mysql2',
110
+ # host: 'localhost',
111
+ # port: 3306},
112
+ # }
113
+ # }
114
+ def config
115
+ @config ||= {
116
+ options: @options,
117
+ environments: environ_config_from_file.each_with_object({}) { |(env_name, env_conf), h|
118
+ conf = env_conf.deep_merge(@options.to_h)
119
+ conf[:adapter] = 'sqlite3' if conf[:adapter].blank?
120
+ conf[:database] = File.expand_path(conf[:database]) if conf[:adapter] == 'sqlite3'
121
+ h[env_name] = conf
122
+ h
123
+ }.with_indifferent_access
124
+ }
98
125
  end
99
126
 
100
127
  def run!
101
128
  show_sql if should_show_sql?
102
129
  write_sql if should_write_sql?
103
130
  append_sql if should_append_sql?
104
- if effective_config[:code].present?
105
- eval(effective_config[:code])
106
- elsif effective_config[:args].present?
107
- effective_config[:args].first.tap { |file| load(file) }
108
- elsif STDIN.isatty
131
+ if @options.code&.present?
132
+ eval(@options.code)
133
+ elsif @options.args.present?
134
+ @options.args.first.tap { |file| load(file) }
135
+ elsif $stdin.isatty
109
136
  run_repl!
110
137
  else
111
- eval(STDIN.read)
138
+ eval($stdin.read)
112
139
  end
113
140
  end
114
141
 
@@ -117,32 +144,32 @@ module Arql
117
144
  end
118
145
 
119
146
  def should_show_sql?
120
- effective_config[:show_sql]
147
+ @options.show_sql
121
148
  end
122
149
 
123
150
  def should_write_sql?
124
- effective_config[:write_sql]
151
+ @options.write_sql
125
152
  end
126
153
 
127
154
  def should_append_sql?
128
- effective_config[:append_sql]
155
+ @options.append_sql
129
156
  end
130
157
 
131
158
  def show_sql
132
159
  App.log_io ||= MultiIO.new
133
160
  ActiveRecord::Base.logger = Logger.new(App.log_io)
134
- App.log_io << STDOUT
161
+ App.log_io << $stdout
135
162
  end
136
163
 
137
164
  def write_sql
138
- write_sql_file = effective_config[:write_sql]
165
+ write_sql_file = @options.write_sql
139
166
  App.log_io ||= MultiIO.new
140
167
  ActiveRecord::Base.logger = Logger.new(App.log_io)
141
168
  App.log_io << File.new(write_sql_file, 'w')
142
169
  end
143
170
 
144
171
  def append_sql
145
- write_sql_file = effective_config[:append_sql]
172
+ write_sql_file = @options.append_sql
146
173
  App.log_io ||= MultiIO.new
147
174
  ActiveRecord::Base.logger = Logger.new(App.log_io)
148
175
  App.log_io << File.new(write_sql_file, 'a')
data/lib/arql/cli.rb CHANGED
@@ -19,7 +19,7 @@ module Arql
19
19
  opts.banner = <<~EOF
20
20
  Usage: arql [options] [ruby file]
21
21
 
22
- If neither [ruby file] nor -e option specified, and STDIN is not a tty, a Pry REPL will be launched,
22
+ If neither [ruby file] nor -e option specified, and STDIN is a tty, a Pry REPL will be launched,
23
23
  otherwise the specified ruby file or -e option value or ruby code read from STDIN will be run, and no REPL launched
24
24
 
25
25
  EOF
@@ -32,8 +32,12 @@ module Arql
32
32
  @options.initializer = initializer
33
33
  end
34
34
 
35
- opts.on('-eENVIRON', '--env=ENVIRON', 'Specify config environment.') do |env|
36
- @options.env = env
35
+ opts.on('-eENVIRON', '--env=ENVIRON', 'Specify config environment, multiple environments allowed, separated by comma') do |env_names|
36
+ @options.environments = env_names.split(/[,\+:]/)
37
+ if @options.environments.any? { |e| e =~ /^default|arql$/i }
38
+ warn '[default, arql] are reserved environment names, please use another name'
39
+ exit(1)
40
+ end
37
41
  end
38
42
 
39
43
  opts.on('-aDB_ADAPTER', '--db-adapter=DB_ADAPTER', 'Specify database Adapter, default is sqlite3') do |db_adapter|
@@ -117,24 +121,36 @@ module Arql
117
121
  end.parse!
118
122
 
119
123
  @options.args = ARGV
124
+
125
+ if @options.environments&.size&.positive? && any_database_options?
126
+ $stderr.puts "Following options are not allowed when using multiple environments specified: #{database_options.join(', ')}"
127
+ $stderr.puts " #{database_options.join(', ')}"
128
+ exit(1)
129
+ end
130
+ end
131
+
132
+ def any_database_options?
133
+ %i[adapter host port database username
134
+ password encoding pool ssh].reduce(false) do |acc, opt|
135
+ acc || @options.send(opt).present?
136
+ end
137
+ end
138
+
139
+ def database_options
140
+ ['--db-adapter', '--db-host', '--db-port', '--db-name', '--db-user', '--db-password',
141
+ '--db-encoding', '--db-pool', '--ssh-host', '--ssh-port', '--ssh-user', '--ssh-password', '--ssh-local-port']
120
142
  end
121
143
 
122
144
  def default_config_file
123
- conf = File.expand_path('~/.arql.yml')
124
- return conf if File.file?(conf)
125
- conf = File.expand_path('~/.arql.yaml')
126
- return conf if File.file?(conf)
127
- conf = File.expand_path('~/.arql.d/init.yml')
128
- return conf if File.file?(conf)
129
- conf = File.expand_path('~/.arql.d/init.yaml')
130
- return conf if File.file?(conf)
145
+ ['~/.arql.yml', '~/.arql.yaml', '~/.arql.d/init.yml', '~/.arql.d/init.yaml'].find { |f|
146
+ File.file?(File.expand_path(f))
147
+ }.try { |f| File.expand_path(f) }
131
148
  end
132
149
 
133
150
  def default_initializer
134
- conf = File.expand_path('~/.arql.rb')
135
- return conf if File.file?(conf)
136
- conf = File.expand_path('~/.arql.d/init.rb')
137
- return conf if File.file?(conf)
151
+ ['~/.arql.rb', '~/.arql.d/init.rb',].find { |f|
152
+ File.file?(File.expand_path(f))
153
+ }.try { |f| File.expand_path(f) }
138
154
  end
139
155
  end
140
156
  end
@@ -3,38 +3,49 @@ require 'rainbow'
3
3
  module Arql::Commands
4
4
  module Info
5
5
  class << self
6
- def db_info
7
- <<~EOF
8
-
9
- Database Connection Information:
10
-
11
- Active: #{color_boolean(ActiveRecord::Base.connection.active?)}
12
- Host: #{Arql::App.config[:host]}
13
- Port: #{Arql::App.config[:port]}
14
- Username: #{Arql::App.config[:username]}
15
- Password: #{(Arql::App.config[:password] || '').gsub(/./, '*')}
16
- Database: #{Arql::App.config[:database]}
17
- Adapter: #{Arql::App.config[:adapter]}
18
- Encoding: #{Arql::App.config[:encoding]}
19
- Pool Size: #{Arql::App.config[:pool]}
20
- EOF
6
+ def db_info(env_name_regexp)
7
+
8
+ Arql::App.instance.definitions.map do |env_name, definition|
9
+ next unless env_name =~ env_name_regexp
10
+ config = Arql::App.config[:environments][env_name]
11
+ <<~DB_INFO
12
+
13
+ #{env_name} Database Connection Information:
14
+
15
+ Active: #{color_boolean(definition.connection.active?)}
16
+ Host: #{config[:host]}
17
+ Port: #{config[:port]}
18
+ Username: #{config[:username]}
19
+ Password: #{(config[:password] || '').gsub(/./, '*')}
20
+ Database: #{config[:database]}
21
+ Adapter: #{config[:adapter]}
22
+ Encoding: #{config[:encoding]}
23
+ Pool Size: #{config[:pool]}
24
+ DB_INFO
25
+ end
21
26
  end
22
27
 
23
- def ssh_info
24
- <<~EOF
28
+ def ssh_info(env_name_regexp)
29
+ Arql::App.instance.definitions.map do |env_name, definition|
30
+ next unless env_name =~ env_name_regexp
31
+ config = Arql::App.config[:environments][env_name]
32
+ next unless config[:ssh].present?
33
+ <<~SSH_INFO
25
34
 
26
- SSH Connection Information:
35
+ #{env_name} SSH Connection Information:
27
36
 
28
- Active: #{color_boolean(Arql::SSHProxy.active?)}
29
- Host: #{Arql::App.config[:ssh][:host]}
30
- Port: #{Arql::App.config[:ssh][:port]}
31
- Username: #{Arql::App.config[:ssh][:user]}
32
- Password: #{(Arql::App.config[:ssh][:password] || '').gsub(/./, '*')}
33
- Local Port: #{Arql::SSHProxy.local_ssh_proxy_port}
34
- EOF
37
+ Active: #{color_boolean(definition.ssh_proxy.active?)}
38
+ Host: #{config[:ssh][:host]}
39
+ Port: #{config[:ssh][:port]}
40
+ Username: #{config[:ssh][:user]}
41
+ Password: #{(config[:ssh][:password] || '').gsub(/./, '*')}
42
+ Local Port: #{definition.ssh_proxy.local_ssh_proxy_port}
43
+ SSH_INFO
44
+ end
35
45
  end
36
46
 
37
47
  private
48
+
38
49
  def color_boolean(bool)
39
50
  if bool
40
51
  Rainbow('TRUE').green
@@ -44,9 +55,11 @@ module Arql::Commands
44
55
  end
45
56
  end
46
57
 
47
- Pry.commands.block_command 'info' do
48
- puts Info::db_info
49
- puts Info::ssh_info if Arql::App.config[:ssh].present?
58
+ Pry.commands.block_command 'info' do |env_name_regexp|
59
+ env_name_regexp ||= '.*'
60
+ env_name_regexp = Regexp.new(env_name_regexp, Regexp::IGNORECASE)
61
+ output.puts Info::db_info(env_name_regexp)
62
+ output.puts Info::ssh_info(env_name_regexp)
50
63
  end
51
64
  end
52
65
  end