punk 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/.document +5 -0
- data/Gemfile +121 -0
- data/Gemfile.lock +353 -0
- data/LICENSE +24 -0
- data/README.md +7 -0
- data/Rakefile +31 -0
- data/VERSION +1 -0
- data/bin/punk +18 -0
- data/lib/punk.rb +32 -0
- data/lib/punk/commands/auth.rb +57 -0
- data/lib/punk/commands/generate.rb +54 -0
- data/lib/punk/commands/http.rb +71 -0
- data/lib/punk/commands/list.rb +28 -0
- data/lib/punk/config/console/defaults.json +5 -0
- data/lib/punk/config/defaults.json +47 -0
- data/lib/punk/config/schema.json +55 -0
- data/lib/punk/config/script/defaults.json +5 -0
- data/lib/punk/config/server/development.json +9 -0
- data/lib/punk/config/spec/defaults.json +5 -0
- data/lib/punk/core/app.rb +233 -0
- data/lib/punk/core/boot.rb +9 -0
- data/lib/punk/core/cli.rb +13 -0
- data/lib/punk/core/commander.rb +82 -0
- data/lib/punk/core/commands.rb +26 -0
- data/lib/punk/core/env.rb +290 -0
- data/lib/punk/core/error.rb +10 -0
- data/lib/punk/core/exec.rb +38 -0
- data/lib/punk/core/interface.rb +76 -0
- data/lib/punk/core/load.rb +9 -0
- data/lib/punk/core/logger.rb +33 -0
- data/lib/punk/core/monkey.rb +7 -0
- data/lib/punk/core/monkey_unreloader.rb +18 -0
- data/lib/punk/core/pry.rb +39 -0
- data/lib/punk/core/settings.rb +38 -0
- data/lib/punk/core/version.rb +7 -0
- data/lib/punk/core/worker.rb +13 -0
- data/lib/punk/framework/action.rb +29 -0
- data/lib/punk/framework/all.rb +10 -0
- data/lib/punk/framework/command.rb +117 -0
- data/lib/punk/framework/model.rb +52 -0
- data/lib/punk/framework/plugins/all.rb +3 -0
- data/lib/punk/framework/plugins/validation.rb +55 -0
- data/lib/punk/framework/runnable.rb +31 -0
- data/lib/punk/framework/service.rb +67 -0
- data/lib/punk/framework/view.rb +26 -0
- data/lib/punk/framework/worker.rb +34 -0
- data/lib/punk/helpers/all.rb +8 -0
- data/lib/punk/helpers/loggable.rb +79 -0
- data/lib/punk/helpers/publishable.rb +9 -0
- data/lib/punk/helpers/renderable.rb +75 -0
- data/lib/punk/helpers/swagger.rb +20 -0
- data/lib/punk/helpers/validatable.rb +57 -0
- data/lib/punk/plugins/all.rb +4 -0
- data/lib/punk/plugins/cors.rb +19 -0
- data/lib/punk/plugins/ssl.rb +13 -0
- data/lib/punk/startup/cache.rb +11 -0
- data/lib/punk/startup/database.rb +57 -0
- data/lib/punk/startup/environment.rb +10 -0
- data/lib/punk/startup/logger.rb +20 -0
- data/lib/punk/startup/task.rb +10 -0
- data/lib/punk/templates/fail.jbuilder +4 -0
- data/lib/punk/templates/fail.rcsv +6 -0
- data/lib/punk/templates/fail.slim +9 -0
- data/lib/punk/templates/fail.xml.slim +6 -0
- data/lib/punk/templates/info.jbuilder +3 -0
- data/lib/punk/templates/info.rcsv +2 -0
- data/lib/punk/templates/info.slim +6 -0
- data/lib/punk/templates/info.xml.slim +3 -0
- data/lib/punk/views/all.rb +4 -0
- data/lib/punk/views/fail.rb +21 -0
- data/lib/punk/views/info.rb +20 -0
- data/punk.gemspec +246 -0
- metadata +747 -0
@@ -0,0 +1,233 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'roda'
|
4
|
+
require 'sidekiq/web'
|
5
|
+
require 'sidekiq/cron/web'
|
6
|
+
require 'rack/protection'
|
7
|
+
|
8
|
+
require_relative '../plugins/all'
|
9
|
+
|
10
|
+
# don't use rack session for sidekiq (we do our own auth when routing)
|
11
|
+
Sidekiq::Web.set(:sessions, false)
|
12
|
+
# TODO: need custom roda route csrf in sidekiq web forms
|
13
|
+
Sidekiq::Web.use(::Rack::Protection, use: :none, logging: true, logger: SemanticLogger['PUNK::RPT'])
|
14
|
+
|
15
|
+
module PUNK
|
16
|
+
def self.route(name, &block)
|
17
|
+
PUNK.profile_info("route", path: name) do
|
18
|
+
PUNK::App.route(name) do |r|
|
19
|
+
r.scope.instance_exec(&block)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
ROUTES = Tempfile.new("routes.json").path
|
25
|
+
PUNK.profile_info("generate", path: ROUTES) do
|
26
|
+
system "roda-parse_routes -f #{ROUTES} #{File.expand_path(File.join(PUNK.get.app.path, 'routes', '*'))}"
|
27
|
+
end
|
28
|
+
|
29
|
+
class App < Roda
|
30
|
+
include Loggable
|
31
|
+
|
32
|
+
PUBLIC = File.join(PUNK.get.app.path, '..', 'www')
|
33
|
+
INDEX = File.read(File.join(PUBLIC, 'index.html'))
|
34
|
+
REMOTE = PUNK.env.staging? || PUNK.env.production?
|
35
|
+
|
36
|
+
plugin :sessions, secret: [PUNK.get.cookie.secret].pack('H*'),
|
37
|
+
key: PUNK.get.cookie.key,
|
38
|
+
max_seconds: 1.year.to_i,
|
39
|
+
max_idle_sessions: 1.month.to_i,
|
40
|
+
cookie_options: {
|
41
|
+
same_site: REMOTE ? :strict : :lax,
|
42
|
+
secure: REMOTE,
|
43
|
+
max_age: 1.year.to_i
|
44
|
+
}
|
45
|
+
plugin :ssl if REMOTE
|
46
|
+
plugin :cors, PUNK.get.app.client
|
47
|
+
# plugin :route_csrf
|
48
|
+
plugin :all_verbs
|
49
|
+
plugin :class_level_routing
|
50
|
+
plugin :delegate
|
51
|
+
plugin :indifferent_params
|
52
|
+
plugin :json_parser
|
53
|
+
plugin :path_rewriter
|
54
|
+
plugin :slash_path_empty
|
55
|
+
plugin :multi_route
|
56
|
+
plugin :type_routing, types: { csv: 'text/csv' }, default_type: :json
|
57
|
+
plugin :route_list, file: ROUTES
|
58
|
+
plugin :disallow_file_uploads
|
59
|
+
plugin :public, root: PUBLIC
|
60
|
+
|
61
|
+
plugin :default_headers, 'Content-Type' => 'text/html', 'Content-Security-Policy' => "default-src 'self';img-src *", 'Strict-Transport-Security' => 'max-age=16070400;', 'X-Frame-Options' => 'deny', 'X-Content-Type-Options' => 'nosniff', 'X-XSS-Protection' => '1; mode=block'
|
62
|
+
|
63
|
+
plugin :error_handler
|
64
|
+
plugin :not_found
|
65
|
+
plugin :hooks
|
66
|
+
|
67
|
+
request_delegate :root, :on, :is, :get, :post, :redirect, :patch, :delete
|
68
|
+
|
69
|
+
rewrite_path(/\A\/?\z/, '/index.html')
|
70
|
+
rewrite_path(/\A\/api\/?\z/, '/api/index.html')
|
71
|
+
|
72
|
+
def require_session!
|
73
|
+
begin
|
74
|
+
# TODO
|
75
|
+
@_current_session = nil #Session[request.session['session_id']]
|
76
|
+
if @_current_session&.active?
|
77
|
+
@_current_session.touch
|
78
|
+
else
|
79
|
+
clear_session
|
80
|
+
@_current_session = nil
|
81
|
+
end
|
82
|
+
rescue StandardError => e
|
83
|
+
exception(e)
|
84
|
+
raise Unauthorized, e.message
|
85
|
+
end
|
86
|
+
raise Unauthorized, 'Session does not exist' if @_current_session.nil?
|
87
|
+
PUNK.logger.info "require_session!", { current_session: current_session.inspect, current_identity: current_identity.inspect, current_user: current_user.inspect }.inspect
|
88
|
+
end
|
89
|
+
|
90
|
+
def require_anonymous!
|
91
|
+
raise BadRequest, "Session already exists" if request.session.present?
|
92
|
+
PUNK.logger.info "require_anonymous!"
|
93
|
+
end
|
94
|
+
|
95
|
+
def require_tenant!
|
96
|
+
raise Unauthorized, 'Session does not exist' if @_current_session.nil?
|
97
|
+
@_current_tenant = current_user.tenants_dataset[id: params[:tenant_id]]
|
98
|
+
PUNK.logger.info "require_tenant!", { current_tenant: @_current_tenant.inspect }.inspect
|
99
|
+
end
|
100
|
+
|
101
|
+
def args
|
102
|
+
params.transform_keys(&:to_sym)
|
103
|
+
end
|
104
|
+
|
105
|
+
def current_session
|
106
|
+
@_current_session
|
107
|
+
end
|
108
|
+
|
109
|
+
def current_identity
|
110
|
+
@_current_session&.identity
|
111
|
+
end
|
112
|
+
|
113
|
+
def current_user
|
114
|
+
@_current_session&.user
|
115
|
+
end
|
116
|
+
|
117
|
+
def current_tenant
|
118
|
+
@_current_tenant
|
119
|
+
end
|
120
|
+
|
121
|
+
def perform(action_class, **kwargs)
|
122
|
+
raise InternalServerError, "Not an action: #{action_class}" unless action_class < Action
|
123
|
+
render action_class.perform(**kwargs)
|
124
|
+
end
|
125
|
+
|
126
|
+
def present(view_class, **kwargs)
|
127
|
+
raise InternalServerError, "Not a view: #{view_class}" unless view_class < View
|
128
|
+
render view_class.present(**kwargs)
|
129
|
+
end
|
130
|
+
|
131
|
+
PUNK_CONTENT_TYPE_LOOKUP = {
|
132
|
+
csv: 'text/csv',
|
133
|
+
html: 'text/html',
|
134
|
+
json: 'application/json',
|
135
|
+
xml: 'application/xml'
|
136
|
+
}.freeze
|
137
|
+
def render(view)
|
138
|
+
raise InternalServerError, "Not a view: #{view}" unless view.is_a?(View)
|
139
|
+
format = request.requested_type
|
140
|
+
view.profile_info("render", format: format) do
|
141
|
+
response.status = view.status if view.is_a?(Fail)
|
142
|
+
response['Content-Type'] = PUNK_CONTENT_TYPE_LOOKUP[format]
|
143
|
+
view.render(format)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
error do |e|
|
148
|
+
exception(e, current_user: current_user)
|
149
|
+
case e
|
150
|
+
when BadRequest
|
151
|
+
present Fail, message: 'Bad Request', error_messages: [e.message], status: 400
|
152
|
+
when Unauthorized
|
153
|
+
present Fail, message: "Unauthorized", error_messages: [e.message], status: 401
|
154
|
+
when Forbidden
|
155
|
+
present Fail, message: "Forbidden", error_messages: [e.message], status: 403
|
156
|
+
when NotFound
|
157
|
+
present Fail, message: "Not Found", error_messages: [e.message], status: 404
|
158
|
+
when NotImplemented
|
159
|
+
present Fail, message: "Not Implemented", error_messages: [e.message], status: 501
|
160
|
+
when InternalServerError
|
161
|
+
present Fail, message: "Internal Server Error", error_messages: [e.message], status: 500
|
162
|
+
else
|
163
|
+
present Fail, message: "Unknown Failure", error_messages: [e.message], status: 500
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
not_found do
|
168
|
+
raise NotFound, "Cannot serve #{request.path}" unless request.is_get?
|
169
|
+
response.status = 200
|
170
|
+
INDEX
|
171
|
+
end
|
172
|
+
|
173
|
+
before do
|
174
|
+
@_started = Time.now.utc
|
175
|
+
name = "#{request.request_method} #{request.path}"
|
176
|
+
logger.info "Started #{name} for #{request.ip}", params.deep_symbolize_keys.sanitize.inspect
|
177
|
+
logger.trace request.env['HTTP_USER_AGENT']
|
178
|
+
# TODO
|
179
|
+
# logger.info "Started #{name} for #{request.ip || Session.default_values[:remote_addr].to_s}", params.deep_symbolize_keys.sanitize.inspect
|
180
|
+
# logger.trace request.env['HTTP_USER_AGENT'] || Session.default_values[:user_agent]
|
181
|
+
logger.trace request.env['HTTP_COOKIE']
|
182
|
+
logger.push_tags(name)
|
183
|
+
_set_cookie(request.env)
|
184
|
+
end
|
185
|
+
|
186
|
+
after do |response|
|
187
|
+
status, headers, _body = response
|
188
|
+
name = logger.pop_tags(1).join
|
189
|
+
duration = 1000.0 * (Time.now.utc - @_started)
|
190
|
+
logger.pop_tags
|
191
|
+
logger.trace((headers.present? ? headers['Set-Cookie'] : nil) || "(no cookie set)")
|
192
|
+
logger.info message: "Completed #{name} status #{status}", duration: duration
|
193
|
+
_store_cookie(headers)
|
194
|
+
end
|
195
|
+
|
196
|
+
route do |r|
|
197
|
+
r.public
|
198
|
+
r.on 'jobs' do
|
199
|
+
require_session!
|
200
|
+
r.run Sidekiq::Web
|
201
|
+
end
|
202
|
+
r.multi_route
|
203
|
+
end
|
204
|
+
|
205
|
+
private
|
206
|
+
|
207
|
+
def _set_cookie(headers)
|
208
|
+
return if REMOTE
|
209
|
+
return if headers.blank?
|
210
|
+
return if headers['HTTP_USER_AGENT'].present?
|
211
|
+
return if headers['HTTP_COOKIE'].present?
|
212
|
+
cookie_file = 'tmp/.cookie-jar'
|
213
|
+
cookie_file += '-test' if PUNK.env.test?
|
214
|
+
cookie = File.read(cookie_file) if File.exist?(cookie_file)
|
215
|
+
return if cookie.blank?
|
216
|
+
headers['HTTP_COOKIE'] = cookie
|
217
|
+
end
|
218
|
+
|
219
|
+
def _store_cookie(headers)
|
220
|
+
return if REMOTE
|
221
|
+
return if headers.blank?
|
222
|
+
cookie = headers['Set-Cookie']
|
223
|
+
return if cookie.blank?
|
224
|
+
cookie_file = 'tmp/.cookie-jar'
|
225
|
+
cookie_file += '-test' if PUNK.env.test?
|
226
|
+
if cookie =~ /max-age=0/
|
227
|
+
File.delete(cookie_file)
|
228
|
+
else
|
229
|
+
File.open(cookie_file, 'w') { |file| file << cookie }
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../punk'
|
4
|
+
|
5
|
+
require 'highline/import'
|
6
|
+
|
7
|
+
PUNK.init(task: 'console', config: { app: { name: 'Punk!' } }).exec
|
8
|
+
|
9
|
+
PUNK.commands(:pry)
|
10
|
+
|
11
|
+
say HighLine.color('Commands: "reload!", "perform [action]", "present [view]", "run [service]". Type "help punk" for more.', :green, :bold)
|
12
|
+
|
13
|
+
PUNK.db.loggers.first.level = :debug
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
command :test do |c|
|
4
|
+
c.description = 'Run rubocop and rspec'
|
5
|
+
c.action do
|
6
|
+
say('Running tests...')
|
7
|
+
warn('!!! PUNK_ENV should be test !!!') unless ENV['PUNK_ENV'] == 'test'
|
8
|
+
ENV.delete_if { |name, _value| name =~ /^PUNK_/ }
|
9
|
+
system('rubocop') &&
|
10
|
+
system('quasar build -m pwa') &&
|
11
|
+
system('PUNK_ENV=test rspec')
|
12
|
+
exit $CHILD_STATUS.exitstatus # rubocop:disable Rails/Exit
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
command :console do |c|
|
17
|
+
c.description = 'Launch the console'
|
18
|
+
c.action do
|
19
|
+
say(HighLine.color('🤘 Are you ready to rock?', :green, :bold))
|
20
|
+
path = File.join(__dir__, 'cli.rb')
|
21
|
+
exec "pry -r #{path}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
alias_command :c, :console
|
25
|
+
|
26
|
+
command :server do |c|
|
27
|
+
c.description = 'Start the server'
|
28
|
+
c.action do
|
29
|
+
say('Starting server...')
|
30
|
+
exec 'puma -C ./config/puma.rb'
|
31
|
+
end
|
32
|
+
end
|
33
|
+
alias_command :s, :server
|
34
|
+
|
35
|
+
command :worker do |c|
|
36
|
+
c.description = 'Start the worker'
|
37
|
+
c.action do
|
38
|
+
say('Starting worker...')
|
39
|
+
path = File.join(__dir__, 'worker.rb')
|
40
|
+
exec "sidekiq -r #{path} -C ./config/sidekiq.yml"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
alias_command :w, :worker
|
44
|
+
|
45
|
+
command 'env dump' do |c|
|
46
|
+
c.description = 'Display the environment'
|
47
|
+
c.action do
|
48
|
+
say('Dumping env...')
|
49
|
+
ap PUNK.get # rubocop:disable Rails/Output
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
command 'db create' do |c|
|
54
|
+
c.description = 'Create the database (dropping it first if necessary)'
|
55
|
+
c.action do
|
56
|
+
say('Creating db...')
|
57
|
+
require 'sequel'
|
58
|
+
database = File.basename(PUNK.get.db.url)
|
59
|
+
server = PUNK.get.db.url.gsub(/[^\/]+$/, 'postgres')
|
60
|
+
pg = Sequel.connect(server)
|
61
|
+
pg.execute "DROP DATABASE IF EXISTS #{database}"
|
62
|
+
pg.execute "CREATE DATABASE #{database}"
|
63
|
+
pg.disconnect
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
command 'db migrate' do |c|
|
68
|
+
c.description = 'Run database migrations'
|
69
|
+
c.option '-r', 'Number of versions to migrate, +ve for up and -ve for down', '--relative NUMBER', Integer
|
70
|
+
c.action do |_args, options|
|
71
|
+
say('Migrating db...')
|
72
|
+
PUNK.boot
|
73
|
+
Sequel.extension :migration
|
74
|
+
if options.relative.nil?
|
75
|
+
Sequel::Migrator.run(PUNK.db, File.join(PUNK.get.app.path, 'migrations'))
|
76
|
+
else
|
77
|
+
Sequel::Migrator.run(PUNK.db, File.join(PUNK.get.app.path, 'migrations'), relative: options.relative)
|
78
|
+
end
|
79
|
+
database = File.basename(PUNK.get.db.url)
|
80
|
+
`pg_dump #{database} --schema-only > schema.psql`
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../framework/command'
|
4
|
+
|
5
|
+
module PUNK
|
6
|
+
def self.commands(target, scope=nil)
|
7
|
+
path = File.expand_path(File.join(__dir__, '..', 'commands'))
|
8
|
+
PUNK.profile_debug("commands", path: path) do
|
9
|
+
Dir.glob(File.join(path, '**/*.rb')).sort.each { |file| require(file) }
|
10
|
+
end
|
11
|
+
path = File.expand_path(File.join(PUNK.get.app.path, 'commands'))
|
12
|
+
PUNK.profile_debug("commands", path: path) do
|
13
|
+
Dir.glob(File.join(path, '**/*.rb')).sort.each { |file| require(file) }
|
14
|
+
end
|
15
|
+
case target
|
16
|
+
when :commander
|
17
|
+
require_relative 'commander'
|
18
|
+
Command.commander
|
19
|
+
when :pry
|
20
|
+
require_relative 'pry'
|
21
|
+
Command.pry
|
22
|
+
when :spec
|
23
|
+
Command.spec(scope)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,290 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/string_inquirer'
|
4
|
+
|
5
|
+
require 'date'
|
6
|
+
require 'fileutils'
|
7
|
+
require 'dotenv'
|
8
|
+
|
9
|
+
module PUNK
|
10
|
+
class Env < Settings
|
11
|
+
def logger
|
12
|
+
SemanticLogger['PUNK::Env']
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(*args)
|
16
|
+
super(*args)
|
17
|
+
return unless args.empty?
|
18
|
+
@loaded = false
|
19
|
+
parent_methods = Module.new do
|
20
|
+
def to_s
|
21
|
+
raise InternalServerError, "Environment not yet loaded" unless @loaded
|
22
|
+
env.to_s
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_sym
|
26
|
+
raise InternalServerError, "Environment not yet loaded" unless @loaded
|
27
|
+
env.to_sym
|
28
|
+
end
|
29
|
+
|
30
|
+
def env
|
31
|
+
ActiveSupport::StringInquirer.new(self[:env].to_s)
|
32
|
+
end
|
33
|
+
|
34
|
+
def task
|
35
|
+
ActiveSupport::StringInquirer.new(self[:task].to_s)
|
36
|
+
end
|
37
|
+
|
38
|
+
def load!
|
39
|
+
return if @loaded
|
40
|
+
_load
|
41
|
+
@loaded = true
|
42
|
+
end
|
43
|
+
end
|
44
|
+
extend(parent_methods)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def _load
|
50
|
+
_load_environment(File.join(PUNK.store.args.path, '..', 'env'), ENV.fetch('PUNK_ENV'), PUNK.store.args.task)
|
51
|
+
@schema = {}
|
52
|
+
_load_schemas(File.join(__dir__, '..', 'config'), ENV.fetch('PUNK_ENV'), PUNK.store.args.task)
|
53
|
+
_load_schemas(File.join(PUNK.store.args.path, 'config'), ENV.fetch('PUNK_ENV'), PUNK.store.args.task)
|
54
|
+
@values = @schema.keys.zip(Array.new(@schema.length, nil)).to_h
|
55
|
+
_add_config(
|
56
|
+
task: PUNK.store.args.task,
|
57
|
+
app: {
|
58
|
+
path: PUNK.store.args.path
|
59
|
+
}
|
60
|
+
)
|
61
|
+
_load_configs(File.join(__dir__, '..', 'config'), ENV.fetch('PUNK_ENV'), PUNK.store.args.task)
|
62
|
+
_load_configs(File.join(PUNK.store.args.path, 'config'), ENV.fetch('PUNK_ENV'), PUNK.store.args.task)
|
63
|
+
_add_environment
|
64
|
+
_add_arguments
|
65
|
+
_validate
|
66
|
+
load(_unflatten(@values))
|
67
|
+
@loaded = true
|
68
|
+
end
|
69
|
+
|
70
|
+
def _flatten(data)
|
71
|
+
results = []
|
72
|
+
data.each do |key, value|
|
73
|
+
if value.is_a?(Hash)
|
74
|
+
_flatten(value).each do |k, v|
|
75
|
+
results << [[key.to_sym, k].flatten, v]
|
76
|
+
end
|
77
|
+
else
|
78
|
+
results << [[key.to_sym], value]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
results.to_h
|
82
|
+
end
|
83
|
+
|
84
|
+
def _unflatten(data)
|
85
|
+
results = {}
|
86
|
+
data.each do |key, value|
|
87
|
+
name = key.join('.')
|
88
|
+
search = results
|
89
|
+
while key.length > 1
|
90
|
+
item = key.shift
|
91
|
+
search[item] ||= {}
|
92
|
+
unless search[item].is_a?(Hash)
|
93
|
+
logger.warn "Skipping #{name} due to conflicted nesting"
|
94
|
+
next
|
95
|
+
end
|
96
|
+
search = search[item]
|
97
|
+
end
|
98
|
+
search[key[0]] = value
|
99
|
+
end
|
100
|
+
results
|
101
|
+
end
|
102
|
+
|
103
|
+
def _load_schema(path)
|
104
|
+
return unless File.exist?(path)
|
105
|
+
logger.trace "Loading schema #{path}..."
|
106
|
+
_flatten(ActiveSupport::JSON.decode(File.read(path))).each do |key, value|
|
107
|
+
override = true
|
108
|
+
required = false
|
109
|
+
key.map! do |name|
|
110
|
+
match = /^(_?)([a-z_]+)(!?)$/.match(name)
|
111
|
+
raise InternalServerError, "Invalid schema key: #{key.join('.')}" if match.nil?
|
112
|
+
override &&= match[1] != '_'
|
113
|
+
required ||= match[3] == '!'
|
114
|
+
match[2].to_sym
|
115
|
+
end
|
116
|
+
raise InternalServerError, "Duplicate schema key: #{key.join('.')}" if @schema.key?(key)
|
117
|
+
@schema[key] = {
|
118
|
+
required: required,
|
119
|
+
override: override,
|
120
|
+
validate: value
|
121
|
+
}
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def _load_schemas(base, name=nil, dir=nil)
|
126
|
+
base = File.expand_path(base)
|
127
|
+
_load_schema(File.join(base, 'schema.json'))
|
128
|
+
_load_schema(File.join(base, "schema_#{name}.json")) unless name.nil?
|
129
|
+
_load_schemas(File.join(base, dir), name) unless dir.nil?
|
130
|
+
end
|
131
|
+
|
132
|
+
def _validate
|
133
|
+
@schema.each do |key, value|
|
134
|
+
next unless value[:required]
|
135
|
+
current_value = @values[key]
|
136
|
+
raise InternalServerError, "Missing required configuration value: #{key.join('.')}" if current_value.nil?
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def _typecast(key, value, validation, required)
|
141
|
+
valid = false
|
142
|
+
match = /^\$([A-Z][A-Z_]+[A-Z])$/.match(value) if value.is_a?(String)
|
143
|
+
if match
|
144
|
+
value = ENV.fetch(match[1], nil)
|
145
|
+
raise InternalServerError, "Env var does not exist: #{match[1]}" if value.nil? && required
|
146
|
+
end
|
147
|
+
value =
|
148
|
+
case validation
|
149
|
+
when /^Enum\((.*)\)$/
|
150
|
+
values = Regexp.last_match(1).split(/ *, */)
|
151
|
+
valid = values.include?(value)
|
152
|
+
value.to_sym
|
153
|
+
when /^Symbol\(\/(.*)\/\)$/
|
154
|
+
regexp = Regexp.last_match(1)
|
155
|
+
valid = value =~ /#{regexp}/
|
156
|
+
value.to_sym
|
157
|
+
when /^Symbol$/
|
158
|
+
valid = true
|
159
|
+
value.to_sym
|
160
|
+
when /^String\(\/(.*)\/\)$/
|
161
|
+
regexp = Regexp.last_match(1)
|
162
|
+
valid = value =~ /#{regexp}/
|
163
|
+
value.to_s
|
164
|
+
when /^String$/
|
165
|
+
valid = true
|
166
|
+
value.to_s
|
167
|
+
when /^Dir\(\/(.*)\/\)$/
|
168
|
+
regexp = Regexp.last_match(1)
|
169
|
+
valid = value =~ /#{regexp}/
|
170
|
+
FileUtils.mkdir_p(value)
|
171
|
+
valid &&= Dir.exist?(value)
|
172
|
+
value
|
173
|
+
when /^Dir$/
|
174
|
+
FileUtils.mkdir_p(value)
|
175
|
+
valid = Dir.exist?(value)
|
176
|
+
value
|
177
|
+
when /^File\(\/(.*)\/\)$/
|
178
|
+
regexp = Regexp.last_match(1)
|
179
|
+
valid = value =~ /#{regexp}/
|
180
|
+
dir = File.dirname(value)
|
181
|
+
FileUtils.mkdir_p(dir)
|
182
|
+
FileUtils.touch(value)
|
183
|
+
valid &&= File.file?(value)
|
184
|
+
value
|
185
|
+
when /^File$/
|
186
|
+
dir = File.dirname(value)
|
187
|
+
FileUtils.mkdir_p(dir)
|
188
|
+
FileUtils.touch(value)
|
189
|
+
valid = File.file?(value)
|
190
|
+
value
|
191
|
+
when /^Flag$/
|
192
|
+
valid = value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
193
|
+
value
|
194
|
+
when /^Integer$/
|
195
|
+
valid = value.is_a?(Integer)
|
196
|
+
value
|
197
|
+
when /^Float$/
|
198
|
+
valid = value.is_a?(Float) || value.is_a?(Integer)
|
199
|
+
value.to_f
|
200
|
+
when /^Date$/
|
201
|
+
valid = Date.parse(value)
|
202
|
+
value
|
203
|
+
when /^Email$/
|
204
|
+
valid = value =~ /^[^@]+@[^@]+$/
|
205
|
+
value
|
206
|
+
when /^URI$/
|
207
|
+
valid = value =~ /^[a-z]+:\/\/[^ ]+$/
|
208
|
+
value
|
209
|
+
when /^Array$/
|
210
|
+
valid = value.is_a?(Array)
|
211
|
+
value
|
212
|
+
else
|
213
|
+
raise InternalServerError, "Unknown validation: #{validation}"
|
214
|
+
end
|
215
|
+
raise InternalServerError, "#{value} is invalid for #{key.join('.')}: #{validation}" unless valid
|
216
|
+
value
|
217
|
+
end
|
218
|
+
|
219
|
+
def _add_value(key, value)
|
220
|
+
schema = @schema[key]
|
221
|
+
raise InternalServerError, "Configuration not found in schema: #{key.join('.')}" if schema.nil?
|
222
|
+
unless schema[:override]
|
223
|
+
current_value = @values[key]
|
224
|
+
raise InternalServerError, "Cannot override configuration value: #{key.join('.')}" unless current_value.nil?
|
225
|
+
end
|
226
|
+
@values[key] = _typecast(key, value, schema[:validate], schema[:required])
|
227
|
+
end
|
228
|
+
|
229
|
+
def _add_config(data)
|
230
|
+
_flatten(data).each do |key, value|
|
231
|
+
_add_value(key, value)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def _load_config(path)
|
236
|
+
return unless File.exist?(path)
|
237
|
+
logger.trace "Loading config #{path}..."
|
238
|
+
_add_config(ActiveSupport::JSON.decode(File.read(path)))
|
239
|
+
end
|
240
|
+
|
241
|
+
def _load_configs(base, name=nil, dir=nil)
|
242
|
+
base = File.expand_path(base)
|
243
|
+
_load_config(File.join(base, 'defaults.json'))
|
244
|
+
_load_config(File.join(base, "#{name}.json")) unless name.nil?
|
245
|
+
_load_configs(File.join(base, dir), name) unless dir.nil?
|
246
|
+
end
|
247
|
+
|
248
|
+
def _load_dotenv(path)
|
249
|
+
return unless File.exist?(path)
|
250
|
+
logger.trace "Loading dotenv #{path}..."
|
251
|
+
Dotenv.load(path)
|
252
|
+
end
|
253
|
+
|
254
|
+
def _load_environment(base, name=nil, dir=nil)
|
255
|
+
base = File.expand_path(base)
|
256
|
+
_load_environment(File.join(base, dir), name) unless dir.nil?
|
257
|
+
_load_dotenv(File.join(base, 'locals.sh'))
|
258
|
+
_load_dotenv(File.join(base, "#{name}.sh"))
|
259
|
+
_load_dotenv(File.join(base, 'defaults.sh'))
|
260
|
+
end
|
261
|
+
|
262
|
+
def _add_environment
|
263
|
+
ENV.each do |key, value|
|
264
|
+
key = key.downcase
|
265
|
+
match = /^punk_(.*)$/.match(key)
|
266
|
+
next unless match
|
267
|
+
key = match[1].split('_').reject(&:empty?).map(&:to_sym)
|
268
|
+
_add_value(key, value)
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
def _add_arguments
|
273
|
+
_add_config(PUNK.store.args.config)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
def self.env
|
278
|
+
PUNK.get.env
|
279
|
+
end
|
280
|
+
|
281
|
+
def self.task
|
282
|
+
PUNK.get.task
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
PUNK::Interface.register(:get) do
|
287
|
+
PUNK::Env.new
|
288
|
+
end
|
289
|
+
|
290
|
+
PUNK.inject :get
|