arql 0.3.31 → 0.4.0

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