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,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'aasm'
|
4
|
+
|
5
|
+
AASM::Configuration.hide_warnings = true
|
6
|
+
|
7
|
+
module PUNK
|
8
|
+
Model = Class.new(Sequel::Model)
|
9
|
+
Model.def_Model(self)
|
10
|
+
class Model
|
11
|
+
include AASM
|
12
|
+
include Loggable
|
13
|
+
|
14
|
+
plugin :validation_helpers
|
15
|
+
plugin :timestamps, update_on_create: true
|
16
|
+
plugin :boolean_readers
|
17
|
+
plugin :uuid
|
18
|
+
plugin :update_or_create
|
19
|
+
plugin :defaults_setter
|
20
|
+
plugin :touch
|
21
|
+
plugin :many_through_many
|
22
|
+
|
23
|
+
plugin PUNK::Plugins::Validation
|
24
|
+
|
25
|
+
def validate; end
|
26
|
+
|
27
|
+
def inspect
|
28
|
+
id.present? ? "#{id}|#{self}" : to_s
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.sample_dataset(count=1)
|
32
|
+
order(Sequel.lit('random()')).limit(count)
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.sample(count=1)
|
36
|
+
query = sample_dataset(count)
|
37
|
+
count == 1 ? query.first : query.all
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.symbolize(*names)
|
41
|
+
names.each do |name|
|
42
|
+
chain =
|
43
|
+
Module.new do
|
44
|
+
define_method(name) do |*args|
|
45
|
+
super(*args)&.to_sym
|
46
|
+
end
|
47
|
+
end
|
48
|
+
prepend chain
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
require 'uri'
|
4
|
+
require 'phony'
|
5
|
+
|
6
|
+
module PUNK
|
7
|
+
module Plugins
|
8
|
+
module Validation
|
9
|
+
module InstanceMethods
|
10
|
+
def validates_url(atts, opts={})
|
11
|
+
default = { message: "is not a URL" }
|
12
|
+
validatable_attributes(atts, default.merge(opts)) do |_name, value, message|
|
13
|
+
message unless URI::DEFAULT_PARSER.make_regexp.match(value).to_a.compact.length > 2
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def validates_email(atts, opts={})
|
18
|
+
default = { message: "is not an email address" }
|
19
|
+
validatable_attributes(atts, default.merge(opts)) do |_name, value, message|
|
20
|
+
message unless URI::MailTo::EMAIL_REGEXP.match(value)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def validates_phone(atts, opts={})
|
25
|
+
default = { message: "is not a phone number" }
|
26
|
+
validatable_attributes(atts, default.merge(opts)) do |_name, value, message|
|
27
|
+
message unless Phony.plausible?(value)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def validates_parse_id(atts, opts={})
|
32
|
+
default = { message: "is not a Parse ID" }
|
33
|
+
validatable_attributes(atts, default.merge(opts)) do |_name, value, message|
|
34
|
+
message unless /^[[:alnum:]]{10}$/.match(value)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def validates_subdomain(atts, opts={})
|
39
|
+
default = { message: "is not a subdomain" }
|
40
|
+
validatable_attributes(atts, default.merge(opts)) do |_name, value, message|
|
41
|
+
message unless /^[A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])?$/.match(value)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def validates_state(name, state)
|
46
|
+
errors.add(name, "is not in #{state} state") unless self[name].send("#{state}?")
|
47
|
+
end
|
48
|
+
|
49
|
+
def validates_event(name, event)
|
50
|
+
errors.add(name, "may not #{event}") unless self[name].send("may_#{event}?")
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PUNK
|
4
|
+
class Runnable < Settings
|
5
|
+
include Validatable
|
6
|
+
|
7
|
+
def self.args(*args)
|
8
|
+
PUNK.store.runnable ||= {}
|
9
|
+
return PUNK.store.runnable[name] if PUNK.store.runnable.key?(name)
|
10
|
+
PUNK.store.runnable[name] = args
|
11
|
+
end
|
12
|
+
|
13
|
+
def method_missing(key, *args, &block)
|
14
|
+
val = super
|
15
|
+
val = val.to_h if val.class == self.class || val.class.instance_of?(self.class)
|
16
|
+
val
|
17
|
+
end
|
18
|
+
|
19
|
+
def respond_to_missing?(key, *args) # rubocop:disable Lint/UselessMethodDefinition
|
20
|
+
super
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def _init_runnable(kwargs)
|
26
|
+
args = self.class.args || []
|
27
|
+
load(args.zip(Array.new(args.length, nil)).to_h)
|
28
|
+
load(kwargs.select { |k, _| args.include?(k) })
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/string_inquirer'
|
4
|
+
|
5
|
+
module PUNK
|
6
|
+
class Service < Runnable
|
7
|
+
include Loggable
|
8
|
+
include Publishable
|
9
|
+
|
10
|
+
delegate :ready?, to: :_state
|
11
|
+
delegate :invalid?, to: :_state
|
12
|
+
delegate :success?, to: :_state
|
13
|
+
delegate :failure?, to: :_state
|
14
|
+
|
15
|
+
def self.run(**kwargs)
|
16
|
+
service = new
|
17
|
+
service.send(:_init, **kwargs)
|
18
|
+
service.send(:_run)
|
19
|
+
hijack = service.send(:_callbacks)
|
20
|
+
service.is_a?(View) && hijack.is_a?(View) ? hijack : service
|
21
|
+
end
|
22
|
+
|
23
|
+
def result
|
24
|
+
@_result
|
25
|
+
end
|
26
|
+
|
27
|
+
def process
|
28
|
+
raise NotImplemented, "view must provide process method"
|
29
|
+
end
|
30
|
+
|
31
|
+
protected
|
32
|
+
|
33
|
+
def _state
|
34
|
+
ActiveSupport::StringInquirer.new(@_state.to_s)
|
35
|
+
end
|
36
|
+
|
37
|
+
def on_success; end
|
38
|
+
|
39
|
+
def on_failure; end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def _init(**kwargs)
|
44
|
+
@_state = :ready
|
45
|
+
@_result = nil
|
46
|
+
_init_runnable(kwargs)
|
47
|
+
end
|
48
|
+
|
49
|
+
def _run
|
50
|
+
unless ready?
|
51
|
+
errors.add(:raised, e.message)
|
52
|
+
@_state = :failure
|
53
|
+
return
|
54
|
+
end
|
55
|
+
unless valid?
|
56
|
+
@_state = :invalid
|
57
|
+
return
|
58
|
+
end
|
59
|
+
@_result = process
|
60
|
+
@_state = :success
|
61
|
+
end
|
62
|
+
|
63
|
+
def _callbacks
|
64
|
+
success? ? on_success : on_failure
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PUNK
|
4
|
+
class View < Service
|
5
|
+
include Renderable
|
6
|
+
|
7
|
+
def self.present(**kwargs)
|
8
|
+
profile_info("present", kwargs) { run(**kwargs) }
|
9
|
+
end
|
10
|
+
|
11
|
+
def process
|
12
|
+
raise NotImplemented, "view must provide process method"
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def on_success
|
18
|
+
raise InternalServerError, "not a template: #{result}" unless result.is_a?(String)
|
19
|
+
template(result)
|
20
|
+
end
|
21
|
+
|
22
|
+
def on_failure
|
23
|
+
Fail.run(message: "view failed: #{self.class}", error_messages: errors.full_messages, status: invalid? ? 400 : 500) unless is_a?(Fail)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sidekiq'
|
4
|
+
|
5
|
+
module PUNK
|
6
|
+
class Worker < Runnable
|
7
|
+
include Sidekiq::Worker
|
8
|
+
|
9
|
+
def self.perform_now(**kwargs)
|
10
|
+
worker = new
|
11
|
+
worker.define_singleton_method :logger, -> { SemanticLogger['PUNK::SKQ'] }
|
12
|
+
worker.send(:perform, **kwargs)
|
13
|
+
end
|
14
|
+
|
15
|
+
def perform(kwargs={})
|
16
|
+
@_started = Time.now.utc
|
17
|
+
logger.info "Started #{self.class.name}", kwargs.sanitize.inspect
|
18
|
+
logger.push_tags(self.class.name)
|
19
|
+
_init_runnable(kwargs.deep_symbolize_keys)
|
20
|
+
raise BadRequest, "validation failed" unless valid?
|
21
|
+
process
|
22
|
+
nil
|
23
|
+
rescue BadRequest => e
|
24
|
+
logger.error e.message
|
25
|
+
logger.error errors.full_messages.to_sentence
|
26
|
+
raise
|
27
|
+
ensure
|
28
|
+
duration = 1000.0 * (Time.now.utc - @_started)
|
29
|
+
logger.pop_tags
|
30
|
+
logger.info message: "Completed #{self.class.name}", duration: duration
|
31
|
+
SemanticLogger.flush
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PUNK
|
4
|
+
module Loggable
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
def logger
|
8
|
+
self.class.logger
|
9
|
+
end
|
10
|
+
|
11
|
+
def profile_info(name, **kwargs, &block)
|
12
|
+
self.class.profile_info(name, **kwargs, &block)
|
13
|
+
end
|
14
|
+
|
15
|
+
def profile_debug(name, **kwargs, &block)
|
16
|
+
self.class.profile_debug(name, **kwargs, &block)
|
17
|
+
end
|
18
|
+
|
19
|
+
def profile_trace(name, **kwargs, &block)
|
20
|
+
self.class.profile_trace(name, **kwargs, &block)
|
21
|
+
end
|
22
|
+
|
23
|
+
def exception(e, extra={})
|
24
|
+
if ENV.key?('SENTRY_DSN')
|
25
|
+
::Raven.capture_exception(
|
26
|
+
e,
|
27
|
+
message: e.message,
|
28
|
+
extra: extra,
|
29
|
+
transaction: "Punk!"
|
30
|
+
)
|
31
|
+
end
|
32
|
+
_clean(e)
|
33
|
+
logger.error exception: e
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
IGNORE = Gem.path + $LOAD_PATH
|
39
|
+
|
40
|
+
def _clean(e, trim: false)
|
41
|
+
_clean(e.cause, trim: true) if e.cause
|
42
|
+
ignore = IGNORE.reject { |path| e.backtrace.first =~ /#{path}/ }
|
43
|
+
skip = false
|
44
|
+
e.backtrace.map! do |line|
|
45
|
+
if trim || ignore.any? { |path| line =~ /#{path}/ }
|
46
|
+
unless skip
|
47
|
+
skip = true
|
48
|
+
'...'
|
49
|
+
end
|
50
|
+
else
|
51
|
+
skip = false
|
52
|
+
line
|
53
|
+
end
|
54
|
+
end
|
55
|
+
e.backtrace.compact!
|
56
|
+
end
|
57
|
+
|
58
|
+
class_methods do
|
59
|
+
def logger
|
60
|
+
SemanticLogger[name]
|
61
|
+
end
|
62
|
+
|
63
|
+
def profile_info(name, **kwargs)
|
64
|
+
logger.info "Started #{name}", kwargs.sanitize.inspect
|
65
|
+
logger.measure_info("Completed #{name}") { logger.tagged(name) { yield } }
|
66
|
+
end
|
67
|
+
|
68
|
+
def profile_debug(name, **kwargs)
|
69
|
+
logger.debug "Started #{name}", kwargs.sanitize.inspect
|
70
|
+
logger.measure_debug("Completed #{name}") { logger.tagged(name) { yield } }
|
71
|
+
end
|
72
|
+
|
73
|
+
def profile_trace(name, **kwargs)
|
74
|
+
logger.trace "Started #{name}", kwargs.sanitize.inspect
|
75
|
+
logger.measure_trace("Completed #{name}") { logger.tagged(name) { yield } }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'tilt'
|
4
|
+
|
5
|
+
module PUNK
|
6
|
+
module Renderable
|
7
|
+
FORMATS =
|
8
|
+
{
|
9
|
+
html: { renderer: :to_html, extension: 'slim' },
|
10
|
+
json: { renderer: :to_json, extension: 'jbuilder' },
|
11
|
+
csv: { renderer: :to_csv, extension: 'rcsv' },
|
12
|
+
xml: { renderer: :to_xml, extension: 'xml.slim' }
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
def template(name)
|
16
|
+
@template = name
|
17
|
+
end
|
18
|
+
|
19
|
+
def render(format)
|
20
|
+
raise NotFound, "unknown format '#{format}'" unless FORMATS.key?(format)
|
21
|
+
send(FORMATS[format][:renderer])
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_html(options={})
|
25
|
+
_render(:html, options)
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_json(options={})
|
29
|
+
_render(:json, options)
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_csv(options={})
|
33
|
+
_render(:csv, options)
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_xml(options={})
|
37
|
+
_render(:xml, options)
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_s
|
41
|
+
to_json
|
42
|
+
end
|
43
|
+
|
44
|
+
def inspect
|
45
|
+
to_s
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_h
|
49
|
+
ActiveSupport::JSON.decode(to_json).to_h.deep_symbolize_keys
|
50
|
+
end
|
51
|
+
|
52
|
+
protected
|
53
|
+
|
54
|
+
def _dir
|
55
|
+
File.join(PUNK.get.app.path, 'templates')
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def _path(format)
|
61
|
+
raise InternalServerError, "No template given" unless @template
|
62
|
+
base = File.join(_dir, @template)
|
63
|
+
ext = FORMATS[format][:extension]
|
64
|
+
"#{base}.#{ext}"
|
65
|
+
end
|
66
|
+
|
67
|
+
def _render(format, options)
|
68
|
+
path = _path(format)
|
69
|
+
raise NotImplemented, "No path for template: #{@template}" unless path
|
70
|
+
Tilt.new(path).render(self, options)
|
71
|
+
rescue LoadError, Errno::ENOENT
|
72
|
+
raise NotFound, "Cannot load template: #{path}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|