punk 0.0.1

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.
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