plezi 0.12.22 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (108) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/LICENSE.txt +17 -18
  4. data/README.md +54 -698
  5. data/Rakefile +7 -4
  6. data/bin/config.ru +22 -0
  7. data/{test → bin}/console +4 -6
  8. data/bin/hello_world +52 -0
  9. data/bin/setup +8 -0
  10. data/exe/plezi +145 -0
  11. data/lib/plezi.rb +24 -137
  12. data/lib/plezi/activation.rb +28 -0
  13. data/lib/plezi/api.rb +62 -0
  14. data/lib/plezi/controller/controller.rb +259 -0
  15. data/lib/plezi/controller/controller_class.rb +176 -0
  16. data/lib/plezi/controller/cookies.rb +40 -0
  17. data/lib/plezi/helpers.rb +60 -0
  18. data/lib/plezi/rake.rb +2 -24
  19. data/lib/plezi/render/erb.rb +34 -0
  20. data/lib/plezi/render/has_cache.rb +36 -0
  21. data/lib/plezi/render/markdown.rb +63 -0
  22. data/lib/plezi/render/render.rb +49 -0
  23. data/lib/plezi/render/sass.rb +55 -0
  24. data/lib/plezi/render/slim.rb +33 -0
  25. data/lib/plezi/router/adclient.rb +23 -0
  26. data/lib/plezi/router/assets.rb +67 -0
  27. data/lib/plezi/router/errors.rb +29 -0
  28. data/lib/plezi/router/route.rb +112 -0
  29. data/lib/plezi/router/router.rb +120 -0
  30. data/lib/plezi/version.rb +1 -1
  31. data/lib/plezi/websockets/message_dispatch.rb +91 -0
  32. data/lib/plezi/websockets/redis.rb +55 -0
  33. data/plezi.gemspec +25 -16
  34. data/resources/404.erb +5 -4
  35. data/resources/500.erb +5 -4
  36. data/resources/{500.html → 503.html} +8 -9
  37. data/resources/client.js +253 -0
  38. data/resources/config.ru +5 -36
  39. data/resources/ctrlr.rb +34 -0
  40. data/resources/gemfile +4 -0
  41. data/resources/mini_app.rb +28 -82
  42. data/resources/mini_exec +7 -0
  43. data/resources/mini_welcome_page.html +0 -0
  44. data/resources/procfile +3 -0
  45. data/resources/rakefile +4 -8
  46. data/resources/routes.rb +9 -26
  47. data/resources/{websockets.js → simple-client.js} +3 -3
  48. metadata +60 -85
  49. data/bin/plezi +0 -104
  50. data/docs/async_helpers.md +0 -245
  51. data/docs/controllers.md +0 -27
  52. data/docs/logging.md +0 -49
  53. data/docs/routes.md +0 -209
  54. data/docs/websockets.md +0 -213
  55. data/lib/plezi/builders/ac_model.rb +0 -59
  56. data/lib/plezi/builders/app_builder.rb +0 -137
  57. data/lib/plezi/builders/builder.rb +0 -43
  58. data/lib/plezi/builders/form_builder.rb +0 -27
  59. data/lib/plezi/common/api.rb +0 -92
  60. data/lib/plezi/common/cache.rb +0 -122
  61. data/lib/plezi/common/defer.rb +0 -21
  62. data/lib/plezi/common/dsl.rb +0 -94
  63. data/lib/plezi/common/redis.rb +0 -65
  64. data/lib/plezi/common/renderer.rb +0 -141
  65. data/lib/plezi/common/settings.rb +0 -52
  66. data/lib/plezi/handlers/controller_core.rb +0 -106
  67. data/lib/plezi/handlers/controller_magic.rb +0 -284
  68. data/lib/plezi/handlers/http_router.rb +0 -205
  69. data/lib/plezi/handlers/placebo.rb +0 -112
  70. data/lib/plezi/handlers/route.rb +0 -216
  71. data/lib/plezi/handlers/session.rb +0 -109
  72. data/lib/plezi/handlers/stubs.rb +0 -156
  73. data/lib/plezi/handlers/ws_identity.rb +0 -253
  74. data/lib/plezi/handlers/ws_object.rb +0 -308
  75. data/lib/plezi/helpers/http_sender.rb +0 -84
  76. data/lib/plezi/helpers/magic_helpers.rb +0 -104
  77. data/lib/plezi/helpers/mime_types.rb +0 -1995
  78. data/lib/plezi/oauth.rb +0 -5
  79. data/lib/plezi/oauth/auth_controller.rb +0 -229
  80. data/logo/dark.png +0 -0
  81. data/logo/light.png +0 -0
  82. data/logo/sign.png +0 -0
  83. data/resources/404.haml +0 -121
  84. data/resources/404.html +0 -124
  85. data/resources/404.slim +0 -120
  86. data/resources/500.haml +0 -120
  87. data/resources/500.slim +0 -120
  88. data/resources/Gemfile +0 -86
  89. data/resources/code.rb +0 -8
  90. data/resources/controller.rb +0 -142
  91. data/resources/database.yml +0 -33
  92. data/resources/db_ac_config.rb +0 -59
  93. data/resources/db_dm_config.rb +0 -51
  94. data/resources/db_sequel_config.rb +0 -33
  95. data/resources/en.yml +0 -204
  96. data/resources/haml_config.rb +0 -6
  97. data/resources/i18n_config.rb +0 -14
  98. data/resources/initialize.rb +0 -49
  99. data/resources/mini_exec.rb +0 -7
  100. data/resources/oauth_config.rb +0 -24
  101. data/resources/plezi_client.js +0 -198
  102. data/resources/plezi_websockets.html +0 -47
  103. data/resources/redis_config.rb +0 -42
  104. data/resources/slim_config.rb +0 -11
  105. data/resources/welcome_page.html +0 -272
  106. data/test/dispatch +0 -58
  107. data/test/hello_world +0 -13
  108. data/test/plezi_tests.rb +0 -581
@@ -0,0 +1,29 @@
1
+ module Plezi
2
+ module Base
3
+ class Err404Ctrl
4
+ def index
5
+ response.status = 404
6
+ render('404') || 'Error 404, not found.'
7
+ end
8
+
9
+ def requested_method
10
+ :index
11
+ end
12
+
13
+ include Plezi::Controller
14
+ end
15
+ class Err500Ctrl
16
+ def index
17
+ response.status = 500
18
+ render('500') || 'Internal Error 500.'
19
+ rescue
20
+ 'Internal Error 500.'
21
+ end
22
+
23
+ def requested_method
24
+ :index
25
+ end
26
+ include Plezi::Controller
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,112 @@
1
+ require 'plezi/controller/controller'
2
+ require 'thread'
3
+ require 'rack'
4
+ require 'rack/query_parser.rb'
5
+
6
+ module Plezi
7
+ module Base
8
+ class Route
9
+ attr_reader :prefix, :controller, :param_names
10
+
11
+ def initialize(path, controller)
12
+ @route_id = "Route#{object_id.to_s(16)}".to_sym
13
+ m = path.match(/([^\:\(\*]*)(.*)/)
14
+ @prefix = m[1].chomp('/'.freeze)
15
+ if @prefix.nil? || @prefix == ''.freeze
16
+ @prefix = '/'.freeze
17
+ @prefix_length = 1
18
+ else
19
+ @prefix = "/#{@prefix}" if @prefix[0] != '/'.freeze
20
+ @prefix_length = @prefix.length + 1
21
+ end
22
+ @controller = controller
23
+ @param_names = []
24
+ @origial = path.dup.freeze
25
+ path2regex(m[2])
26
+ self.class.qp
27
+ case @controller
28
+ when Class
29
+ prep_controller
30
+ when Regexp
31
+ raise "Rewrite Routes can't contain more then one parameter to collect" if @param_names.length > 1
32
+ else
33
+ raise 'Controller should be a class object' unless controller.is_a?(Class)
34
+ end
35
+ end
36
+
37
+ def call(request, response)
38
+ return nil unless match(request.path_info, request)
39
+ case @controller
40
+ when Class
41
+ c = @controller.new
42
+ return c._pl_respond(request, response, Thread.current[@route_id])
43
+ when Regexp
44
+ params = Thread.current[@route_id]
45
+ return nil unless controller =~ params[@param_names[0]]
46
+ request.path_info = "/#{params.delete('*'.freeze).to_a.join '/'}"
47
+ request.params.update params
48
+ end
49
+ nil
50
+ end
51
+
52
+ def fits_params(path, request)
53
+ params = (Thread.current[@route_id] ||= {}).clear
54
+ params.update request.params.to_h if request && request.params
55
+ # puts "cutting: #{path[(@prefix_length)..-1] ? path[(@prefix_length + 1)..-1] : 'nil'}"
56
+ pa = (path[@prefix_length..-1] || ''.freeze).split('/'.freeze)
57
+ # puts "check param count: #{pa}"
58
+ return false unless @params_range.include?(pa.length)
59
+ @param_names.each do |key|
60
+ next if pa[0].nil?
61
+ self.class.qp.normalize_params(params, Plezi.try_utf8!(Rack::Utils.unescape(key)),
62
+ Plezi.try_utf8!(Rack::Utils.unescape(pa.shift)), 100)
63
+ end
64
+ params['*'.freeze] = pa unless pa.empty?
65
+ true
66
+ end
67
+
68
+ def match(req_path, request = nil)
69
+ # puts "#{req_path} starts with #{@prefix}? #{req_path.start_with?(@prefix)}"
70
+ req_path.start_with?(@prefix) && fits_params(req_path, request)
71
+ end
72
+
73
+ def path2regex(postfix)
74
+ pfa = postfix.split '/'.freeze
75
+ start = 0; stop = 0
76
+ optional = false
77
+ while pfa.any?
78
+ name = pfa.shift
79
+ raise "#{name} is not a valid path section in #{@origial}" if /^((\:[\w\.\[\]]+)|(\(\:[\w\.\[\]]+\))|(\*))$/.match(name).nil?
80
+ if name[0] == ':'
81
+ raise "Cannot have a required parameter after an optional parameter in #{@origial}" if optional
82
+ @param_names << name[1..-1].freeze
83
+ elsif name[0] == '('
84
+ optional = true
85
+ @param_names << name[2..-2].freeze
86
+ elsif name[0] == '*'
87
+ stop += 999_999
88
+ break
89
+ else
90
+ raise "invalid path section #{name} in #{@origial}"
91
+ end
92
+ optional ? (stop += 1) : (start += 1)
93
+ end
94
+ unless (@param_names.include? 'id'.freeze) || stop >= 999_999
95
+ @param_names << 'id'.freeze
96
+ stop += 1
97
+ end
98
+ @params_range = (start..(start + stop))
99
+ @param_names.freeze
100
+ @params_range.freeze
101
+ end
102
+
103
+ def prep_controller
104
+ @controller.include Plezi::Controller
105
+ end
106
+
107
+ def self.qp
108
+ @qp ||= ::Rack::QueryParser.new(Hash, 65_536, 100)
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,120 @@
1
+ require 'plezi/router/route'
2
+ require 'plezi/router/errors'
3
+ require 'plezi/router/assets'
4
+ require 'plezi/router/adclient'
5
+ require 'rack'
6
+
7
+ module Plezi
8
+ module Base
9
+ module Router
10
+ @routes = []
11
+ @app = nil
12
+
13
+ module_function
14
+
15
+ def new(app)
16
+ puts 'Plezi as Middleware'
17
+ @app = ((app == Plezi.app) ? nil : app)
18
+ Plezi.app
19
+ end
20
+
21
+ def call(env)
22
+ request = Rack::Request.new(env)
23
+ response = Rack::Response.new
24
+ ret = nil
25
+ @routes.each { |route| ret = route.call(request, response); break if ret }
26
+ unless ret
27
+ return @app.call(env) if @app
28
+ ret = ::Plezi::Base::Err404Ctrl.new._pl_respond(request, response, request.params)
29
+ end
30
+ response.write(ret) if ret.is_a?(String)
31
+ return response.finish
32
+ rescue => e
33
+ puts e.message, e.backtrace
34
+ response = Rack::Response.new
35
+ response.write ::Plezi::Base::Err500Ctrl.new._pl_respond(request, response, request.params)
36
+ return response.finish
37
+ end
38
+
39
+ def route(path, controller)
40
+ path = path.chomp('/'.freeze) unless path == '/'.freeze
41
+ case controller
42
+ when :client
43
+ controller = ::Plezi::Base::Router::ADClient
44
+ when :assets
45
+ controller = ::Plezi::Base::Assets
46
+ path << '/*'.freeze unless path[-1] == '*'.freeze
47
+ when Regexp
48
+ path << '/*'.freeze unless path[-1] == '*'.freeze
49
+ end
50
+ @routes << Route.new(path, controller)
51
+ end
52
+
53
+ def list
54
+ @routes
55
+ end
56
+
57
+ def url_for(controller, method_sym, params = {})
58
+ # GET,PUT,POST,DELETE
59
+ r = nil
60
+ url = '/'.dup
61
+ @routes.each do |tmp|
62
+ case tmp.controller
63
+ when Class
64
+ next if tmp.controller != controller
65
+ r = tmp
66
+ break
67
+ when Regexp
68
+ nm = nil
69
+ nm = tmp.param_names[0] if params[tmp.param_names[0]]
70
+ nm ||= tmp.param_names[0].to_sym
71
+ url << "#{params.delete nm}/" if params[nm] && params[nm].to_s =~ tmp.controller
72
+ else
73
+ next
74
+ end
75
+ end
76
+ return nil if r.nil?
77
+ case method_sym.to_sym
78
+ when :new
79
+ params.delete :id
80
+ params.delete :_method
81
+ params.delete '_method'.freeze
82
+ params['id'.freeze] = :new
83
+ when :create
84
+ params['id'.freeze] = :new
85
+ params.delete :id
86
+ params['_method'.freeze] = :post
87
+ params.delete :_method
88
+ when :update
89
+ params.delete :_method
90
+ params['_method'.freeze] = :put
91
+ when :delete
92
+ params.delete :_method
93
+ params['_method'.freeze] = :delete
94
+ when :index
95
+ params.delete 'id'.freeze
96
+ params.delete '_method'.freeze
97
+ params.delete :id
98
+ params.delete :_method
99
+ when :show
100
+ raise "The URL for ':show' MUST contain a valid 'id' parameter for the object's index to display." unless params['id'.freeze].nil? && params[:id].nil?
101
+ params.delete '_method'.freeze
102
+ params.delete :_method
103
+ else
104
+ params.delete :id
105
+ params['id'.freeze] = method_sym
106
+ end
107
+ names = r.param_names
108
+ url.chomp! '/'.freeze
109
+ url << r.prefix
110
+ url.clear if url == '/'.freeze
111
+ while names.any? && params[name[0]]
112
+ url << "/#{Rack::Utils.escape params[names.shift]}"
113
+ end
114
+ url = '/'.dup if url.empty?
115
+ (url << '?') << Rack::Utils.build_nested_query(params) if params.any?
116
+ url
117
+ end
118
+ end
119
+ end
120
+ end
data/lib/plezi/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Plezi
2
- VERSION = "0.12.22"
2
+ VERSION = '0.14.0'.freeze
3
3
  end
@@ -0,0 +1,91 @@
1
+ require 'set'
2
+ require 'securerandom'
3
+ require 'yaml'
4
+ module Plezi
5
+ module Base
6
+ module MessageDispatch
7
+ class << self
8
+ # Allows pub/sub drivers to attach to the message dispatch using `MessageDispatch.drivers << driver`
9
+ attr_reader :drivers
10
+ end
11
+ @drivers = [].to_set
12
+
13
+ module_function
14
+
15
+ def pid
16
+ @uuid ||= SecureRandom.urlsafe_base64.tap { |str| @prefix_len = str.length }
17
+ end
18
+
19
+ def _init
20
+ @drivers.each(&:connect)
21
+ end
22
+
23
+ def push(message)
24
+ # message[:type] = message[:type].name if message[:type].is_a?(Class)
25
+ message[:origin] = pid
26
+ hst = message.delete(:host) || Plezi.app_name
27
+ yml = message.to_yaml
28
+ @drivers.each { |d| d.push(hst, yml) }
29
+ end
30
+
31
+ def <<(msg)
32
+ @safe_types ||= [Symbol, Date, Time, Encoding, Struct, Regexp, Range, Set].freeze
33
+ msg = YAML.safe_load(msg, @safe_types)
34
+ return if msg[:origin] == pid
35
+ msg[:type] ||= msg['type'.freeze]
36
+ msg[:type] = Object.const_get msg[:type] if msg[:type] && msg[:type] != :all
37
+ if msg[:target] ||= msg['target'.freeze]
38
+ Iodine::Websocket.defer(target2uuid(msg[:target])) { |ws| ws._pl_ad_review(ws.__send__(ws._pl_ws_map[msg[:event]], *(msg[:args] ||= msg['args'.freeze] || []))) if ws._pl_ws_map[msg[:event] ||= msg['event'.freeze]] }
39
+ elsif (msg[:type]) == :all
40
+ Iodine::Websocket.each { |ws| ws._pl_ad_review(ws.__send__(ws._pl_ws_map[msg[:event]], *(msg[:args] ||= msg['args'.freeze] || []))) if ws._pl_ws_map[msg[:event] ||= msg['event'.freeze]] }
41
+ else
42
+ Iodine::Websocket.each { |ws| ws._pl_ad_review(ws.__send__(ws._pl_ws_map[msg[:event]], *(msg[:args] ||= msg['args'.freeze] || []))) if ws.is_a?(msg[:type]) && msg[:type]._pl_ws_map[msg[:event] ||= msg['event'.freeze]] }
43
+ end
44
+
45
+ rescue => e
46
+ puts '*** The following could be a security breach attempt:', e.message, e.backtrace
47
+ nil
48
+ end
49
+
50
+ def unicast(_sender, target, meth, args)
51
+ return false if target.nil?
52
+ if (tuuid = target2uuid)
53
+ Iodine::Websocket.defer(tuuid) { |ws| ws._pl_ad_review(ws.__send__(ws._pl_ws_map[meth], *args)) if ws._pl_ws_map[meth] }
54
+ return true
55
+ end
56
+ push target: target, args: args, host: target2pid(target)
57
+ end
58
+
59
+ def broadcast(sender, meth, args)
60
+ if sender.is_a?(Class)
61
+ Iodine::Websocket.each { |ws| ws._pl_ad_review(ws.__send__(ws._pl_ws_map[meth], *args)) if ws.is_a?(sender) && ws._pl_ws_map[meth] }
62
+ push type: sender.name, args: args, event: meth
63
+ else
64
+ sender.each { |ws| ws._pl_ad_review(ws.__send__(ws._pl_ws_map[meth], *args)) if ws.is_a?(sender.class) && ws._pl_ws_map[meth] }
65
+ push type: sender.class.name, args: args, event: meth
66
+ end
67
+ end
68
+
69
+ def multicast(sender, meth, args)
70
+ if sender.is_a?(Class)
71
+ Iodine::Websocket.each { |ws| ws._pl_ad_review(ws.__send__(ws._pl_ws_map[meth], *args)) if ws._pl_ws_map[meth] }
72
+ push type: :all, args: args, event: meth
73
+ else
74
+ sender.each { |ws| ws._pl_ad_review(ws.__send__(ws._pl_ws_map[meth], *args)) if ws._pl_ws_map[meth] }
75
+ push type: :all, args: args, event: meth
76
+ end
77
+ end
78
+
79
+ def target2uuid(target)
80
+ return nil unless target.start_with? pid
81
+ target[@prefix_len..-1].to_i
82
+ end
83
+
84
+ def target2pid(target)
85
+ target ? target[0..(@prefix_len - 1)] : Plezi.app_name
86
+ end
87
+ end
88
+ end
89
+ end
90
+ # connect default drivers
91
+ require 'plezi/websockets/redis'
@@ -0,0 +1,55 @@
1
+ require 'securerandom'
2
+ module Plezi
3
+ module Base
4
+ module MessageDispatch
5
+ module RedisDriver
6
+ @redis_locker ||= Mutex.new
7
+ @redis = @redis_sub_thread = nil
8
+
9
+ module_function
10
+
11
+ def connect
12
+ return false unless ENV['PL_REDIS_URL'] && defined?(::Redis)
13
+ return @redis if (@redis_sub_thread && @redis_sub_thread.alive?) && @redis
14
+ @redis_locker.synchronize do
15
+ return @redis if (@redis_sub_thread && @redis_sub_thread.alive?) && @redis # repeat the test inside syncing, things change.
16
+ @redis.quit if @redis
17
+ @redis = ::Redis.new(url: ENV['PL_REDIS_URL'])
18
+ raise "Redis connction failed for: #{ENV['PL_REDIS_URL']}" unless @redis
19
+ @redis_sub_thread = Thread.new do
20
+ begin
21
+ ::Redis.new(url: ENV['PL_REDIS_URL']).subscribe(::Plezi.app_name, ::Plezi::Base::MessageDispatch.pid) do |on|
22
+ on.message do |_channel, msg|
23
+ ::Plezi::Base::MessageDispatch << msg
24
+ end
25
+ end
26
+ rescue => e
27
+ puts e.message, e.backtrace
28
+ retry
29
+ end
30
+ end
31
+ @redis
32
+ end
33
+ end
34
+
35
+ # Get the current redis connection.
36
+ def redis
37
+ @redis || connect
38
+ end
39
+
40
+ def push(channel, message)
41
+ return unless connect
42
+ return if away?(channel)
43
+ redis.publish(channel, message)
44
+ end
45
+
46
+ def away?(server)
47
+ return true unless connect
48
+ @redis.pubsub('CHANNELS', server).empty?
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ ::Plezi::Base::MessageDispatch.drivers << ::Plezi::Base::MessageDispatch::RedisDriver
data/plezi.gemspec CHANGED
@@ -4,25 +4,34 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
  require 'plezi/version'
5
5
 
6
6
  Gem::Specification.new do |spec|
7
- spec.name = "plezi"
7
+ spec.name = 'plezi'
8
8
  spec.version = Plezi::VERSION
9
- spec.authors = ["Boaz Segev"]
10
- spec.email = ['boaz@2be.co.il']
11
- spec.summary = %q{Plezi - the easy way to add Websockets, RESTful routing and HTTP streaming services to Ruby Web-Apps.}
12
- spec.description = %q{Plezi - the easy way to add Websockets, RESTful routing and HTTP streaming services to Ruby Web-Apps.}
13
- spec.homepage = "http://www.plezi.io/"
14
- spec.license = "MIT"
9
+ spec.authors = ['Boaz Segev']
10
+ spec.email = ['bo@plezi.io']
15
11
 
16
- spec.files = `git ls-files -z`.split("\x0")
17
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
- spec.require_paths = ["lib"]
12
+ spec.summary = 'The Plezi.io Ruby Framework for real time web applications.'
13
+ spec.description = 'The Plezi.io Ruby Framework for real time web applications.'
14
+ spec.homepage = 'http://plezi.io'
15
+ spec.license = 'MIT'
20
16
 
21
- spec.add_dependency "iodine", "~> 0.1.20"
22
- spec.add_development_dependency "bundler", "~> 1.7"
23
- spec.add_development_dependency "rake", "~> 10.0"
17
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
18
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
19
+ if spec.respond_to?(:metadata)
20
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
21
+ else
22
+ raise 'RubyGems 2.0 or newer is required to protect against public gem pushes.'
23
+ end
24
24
 
25
- spec.post_install_message = "Thank you for installing Plezi, the native Ruby Framework for real time web-apps."
26
- # spec.post_install_message = "** Deprecation Warning:\n\nThank you for installing Plezi, the native Ruby Framework for real time web-apps."
25
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
26
+ spec.bindir = 'exe'
27
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ['lib']
27
29
 
30
+ spec.add_dependency 'iodine', '~> 0.2.0'
31
+ # spec.add_dependency 'redcarpet', '> 3.3.0'
32
+ # spec.add_dependency 'slim', '> 3.0.0'
33
+
34
+ spec.add_development_dependency 'bundler', '~> 1.12'
35
+ spec.add_development_dependency 'rake', '~> 10.0'
36
+ spec.add_development_dependency 'minitest', '~> 5.0'
28
37
  end