punk 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/.document +5 -0
  3. data/Gemfile +121 -0
  4. data/Gemfile.lock +353 -0
  5. data/LICENSE +24 -0
  6. data/README.md +7 -0
  7. data/Rakefile +31 -0
  8. data/VERSION +1 -0
  9. data/bin/punk +18 -0
  10. data/lib/punk.rb +32 -0
  11. data/lib/punk/commands/auth.rb +57 -0
  12. data/lib/punk/commands/generate.rb +54 -0
  13. data/lib/punk/commands/http.rb +71 -0
  14. data/lib/punk/commands/list.rb +28 -0
  15. data/lib/punk/config/console/defaults.json +5 -0
  16. data/lib/punk/config/defaults.json +47 -0
  17. data/lib/punk/config/schema.json +55 -0
  18. data/lib/punk/config/script/defaults.json +5 -0
  19. data/lib/punk/config/server/development.json +9 -0
  20. data/lib/punk/config/spec/defaults.json +5 -0
  21. data/lib/punk/core/app.rb +233 -0
  22. data/lib/punk/core/boot.rb +9 -0
  23. data/lib/punk/core/cli.rb +13 -0
  24. data/lib/punk/core/commander.rb +82 -0
  25. data/lib/punk/core/commands.rb +26 -0
  26. data/lib/punk/core/env.rb +290 -0
  27. data/lib/punk/core/error.rb +10 -0
  28. data/lib/punk/core/exec.rb +38 -0
  29. data/lib/punk/core/interface.rb +76 -0
  30. data/lib/punk/core/load.rb +9 -0
  31. data/lib/punk/core/logger.rb +33 -0
  32. data/lib/punk/core/monkey.rb +7 -0
  33. data/lib/punk/core/monkey_unreloader.rb +18 -0
  34. data/lib/punk/core/pry.rb +39 -0
  35. data/lib/punk/core/settings.rb +38 -0
  36. data/lib/punk/core/version.rb +7 -0
  37. data/lib/punk/core/worker.rb +13 -0
  38. data/lib/punk/framework/action.rb +29 -0
  39. data/lib/punk/framework/all.rb +10 -0
  40. data/lib/punk/framework/command.rb +117 -0
  41. data/lib/punk/framework/model.rb +52 -0
  42. data/lib/punk/framework/plugins/all.rb +3 -0
  43. data/lib/punk/framework/plugins/validation.rb +55 -0
  44. data/lib/punk/framework/runnable.rb +31 -0
  45. data/lib/punk/framework/service.rb +67 -0
  46. data/lib/punk/framework/view.rb +26 -0
  47. data/lib/punk/framework/worker.rb +34 -0
  48. data/lib/punk/helpers/all.rb +8 -0
  49. data/lib/punk/helpers/loggable.rb +79 -0
  50. data/lib/punk/helpers/publishable.rb +9 -0
  51. data/lib/punk/helpers/renderable.rb +75 -0
  52. data/lib/punk/helpers/swagger.rb +20 -0
  53. data/lib/punk/helpers/validatable.rb +57 -0
  54. data/lib/punk/plugins/all.rb +4 -0
  55. data/lib/punk/plugins/cors.rb +19 -0
  56. data/lib/punk/plugins/ssl.rb +13 -0
  57. data/lib/punk/startup/cache.rb +11 -0
  58. data/lib/punk/startup/database.rb +57 -0
  59. data/lib/punk/startup/environment.rb +10 -0
  60. data/lib/punk/startup/logger.rb +20 -0
  61. data/lib/punk/startup/task.rb +10 -0
  62. data/lib/punk/templates/fail.jbuilder +4 -0
  63. data/lib/punk/templates/fail.rcsv +6 -0
  64. data/lib/punk/templates/fail.slim +9 -0
  65. data/lib/punk/templates/fail.xml.slim +6 -0
  66. data/lib/punk/templates/info.jbuilder +3 -0
  67. data/lib/punk/templates/info.rcsv +2 -0
  68. data/lib/punk/templates/info.slim +6 -0
  69. data/lib/punk/templates/info.xml.slim +3 -0
  70. data/lib/punk/views/all.rb +4 -0
  71. data/lib/punk/views/fail.rb +21 -0
  72. data/lib/punk/views/info.rb +20 -0
  73. data/punk.gemspec +246 -0
  74. metadata +747 -0
@@ -0,0 +1,5 @@
1
+ {
2
+ "log": {
3
+ "level": "info"
4
+ }
5
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "app": {
3
+ "reloadable": true,
4
+ "client": "http://localhost:4000"
5
+ },
6
+ "log": {
7
+ "type": "file"
8
+ }
9
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "log": {
3
+ "level": "info"
4
+ }
5
+ }
@@ -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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../startup/logger'
4
+ require_relative '../startup/environment'
5
+ require_relative '../startup/task'
6
+ require_relative '../startup/database'
7
+ require_relative '../startup/cache'
8
+
9
+ PUNK.store[:state] = :booted
@@ -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