tynn 2.0.0.beta3 → 2.0.0.beta4

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,57 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "tynn/base"
3
+ require "rack"
4
+ require "seg"
5
+ require_relative "tynn/request"
6
+ require_relative "tynn/response"
7
+ require_relative "tynn/utils"
4
8
  require_relative "tynn/version"
5
9
 
6
10
  class Tynn
7
- # Loads given <tt>plugin</tt> into the application.
8
- #
9
- # [plugin]
10
- # A module that can contain a <tt>ClassMethods</tt> or <tt>InstanceMethods</tt>
11
- # module to extend Tynn. If <tt>plugin</tt> responds to <tt>setup</tt>, it will
12
- # be called last, and should be used to set up the plugin.
13
- #
14
- # [*args]
15
- # A list of arguments passed to <tt>plugin#setup</tt>.
16
- #
17
- # [&block]
18
- # A block passed to <tt>plugin#setup</tt>.
19
- #
20
- # <tt></tt>
21
- #
22
- # # Using default plugins
23
- # require "tynn"
24
- # require "tynn/environment"
25
- # require "tynn/static"
26
- #
27
- # Tynn.plugin(Tynn::Environment)
28
- # Tynn.plugin(Tynn::Static, %(/css /js /images))
29
- #
30
- # # Using a custom plugin
31
- # class MyAppNamePlugin
32
- # def self.setup(app, name, &block)
33
- # app.app_name = name
34
- # end
35
- #
36
- # module ClassMethods
37
- # def app_name
38
- # @app_name
39
- # end
40
- #
41
- # def app_name=(name)
42
- # @app_name = name
43
- # end
44
- # end
45
- #
46
- # module InstanceMethods
47
- # def app_name
48
- # self.class.app_name
49
- # end
50
- # end
51
- # end
52
- #
53
- # Tynn.plugin(MyAppNamePlugin, "MyApp")
54
- #
55
11
  def self.plugin(plugin, *args, &block)
56
12
  if defined?(plugin::InstanceMethods)
57
13
  include(plugin::InstanceMethods)
@@ -66,5 +22,185 @@ class Tynn
66
22
  end
67
23
  end
68
24
 
69
- plugin(Tynn::Base)
25
+ module ClassMethods
26
+ def define(&block)
27
+ build_app { |env| new(block).call(env) }
28
+
29
+ app.freeze
30
+
31
+ middleware.freeze
32
+
33
+ Tynn::Utils.deep_freeze!(settings)
34
+ end
35
+
36
+ def build_app(&block)
37
+ @app = middleware.reverse.inject(block) { |a, e| e.call(a) }
38
+ end
39
+
40
+ def use(middleware, *args, &block)
41
+ if self.middleware.frozen?
42
+ Tynn::Utils.raise_error("Application middleware is frozen")
43
+ else
44
+ self.middleware.push(proc { |app| middleware.new(app, *args, &block) })
45
+ end
46
+ end
47
+
48
+ def middleware
49
+ @middleware ||= []
50
+ end
51
+
52
+ def call(env)
53
+ app.call(env)
54
+ end
55
+
56
+ def app
57
+ (defined?(@app) && @app) or
58
+ Tynn::Utils.raise_error("Application handler is missing")
59
+ end
60
+
61
+ def inherited(subclass)
62
+ subclass.instance_variable_set(:@settings, Tynn::Utils.deep_dup(settings))
63
+ end
64
+
65
+ def settings
66
+ @settings ||= {}
67
+ end
68
+
69
+ def set(option, value)
70
+ v = settings[option]
71
+
72
+ if Hash === v
73
+ value = v.merge(value)
74
+ end
75
+
76
+ set!(option, value)
77
+ end
78
+
79
+ def set!(option, value)
80
+ settings[option] = value
81
+ end
82
+
83
+ def default_headers
84
+ settings.fetch(:default_headers, {})
85
+ end
86
+ end
87
+
88
+ # Copyright (c) 2016 Francesco Rodriguez
89
+ # Copyright (c) 2015-2016 Michel Martens (Portions of https://github.com/soveran/syro)
90
+ #
91
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
92
+ # of this software and associated documentation files (the "Software"), to deal
93
+ # in the Software without restriction, including without limitation the rights
94
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
95
+ # copies of the Software, and to permit persons to whom the Software is
96
+ # furnished to do so, subject to the following conditions:
97
+ #
98
+ # The above copyright notice and this permission notice shall be included in
99
+ # all copies or substantial portions of the Software.
100
+ #
101
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
102
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
103
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
104
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
105
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
106
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
107
+ # THE SOFTWARE.
108
+
109
+ module InstanceMethods
110
+ def initialize(code)
111
+ @__code = code
112
+ @__captures = []
113
+ end
114
+
115
+ def call(env)
116
+ @__env = env
117
+ @__req = Tynn::Request.new(env)
118
+ @__res = Tynn::Response.new(nil, Hash[self.class.default_headers])
119
+ @__seg = ::Seg.new(env["PATH_INFO"])
120
+
121
+ catch(:halt) do
122
+ instance_eval(&@__code)
123
+
124
+ @__res.finish
125
+ end
126
+ end
127
+
128
+ def req
129
+ @__req
130
+ end
131
+
132
+ def res
133
+ @__res
134
+ end
135
+
136
+ def on(arg)
137
+ captures.clear
138
+
139
+ if match(arg)
140
+ yield(*captures)
141
+ end
142
+ end
143
+
144
+ private def match(arg)
145
+ case arg
146
+ when String then @__seg.consume(arg)
147
+ when Symbol then @__seg.capture(0, captures)
148
+ when Proc then arg.call
149
+ when true then true
150
+ else false
151
+ end
152
+ end
153
+
154
+ private def captures
155
+ @__captures
156
+ end
157
+
158
+ def halt(response)
159
+ throw(:halt, response)
160
+ end
161
+
162
+ def run(app, vars = nil)
163
+ path, script = @__env["PATH_INFO"], @__env["SCRIPT_NAME"]
164
+
165
+ @__env["PATH_INFO"] = @__seg.curr
166
+ @__env["SCRIPT_NAME"] = @__seg.prev
167
+
168
+ @__env.delete("tynn.vars")
169
+ @__env["tynn.vars"] = vars if vars
170
+
171
+ halt(app.call(@__env))
172
+ ensure
173
+ @__env["PATH_INFO"], @__env["SCRIPT_NAME"] = path, script
174
+ end
175
+
176
+ def vars
177
+ @__env.fetch("tynn.vars", {})
178
+ end
179
+
180
+ def get
181
+ root? && req.get?
182
+ end
183
+
184
+ def post
185
+ root? && req.post?
186
+ end
187
+
188
+ def patch
189
+ root? && req.patch?
190
+ end
191
+
192
+ def put
193
+ root? && req.put?
194
+ end
195
+
196
+ def delete
197
+ root? && req.delete?
198
+ end
199
+
200
+ def root?
201
+ @__seg.root?
202
+ end
203
+ end
204
+
205
+ plugin(self)
70
206
  end
@@ -1,115 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Tynn
4
- # Adds helper methods to get and check the current environment.
5
- # By default, the environment is based on <tt>ENV["RACK_ENV"]</tt>.
6
- #
7
- # require "tynn"
8
- # require "tynn/environment"
9
- #
10
- # Tynn.plugin(Tynn::Environment)
11
- #
12
- # # Accessing the current environment.
13
- # Tynn.environment # => :development
14
- #
15
- # # Checking the current environment.
16
- # Tynn.development? # => true
17
- # Tynn.production? # => false
18
- # Tynn.test? # => false
19
- # Tynn.staging? # => false
20
- #
21
- # # Changing the current environment.
22
- # Tynn.set(:environment, :test)
23
- #
24
- # # Performing operations in specific environments.
25
- # Tynn.configure(:development, :test) do
26
- # # ...
27
- # end
28
- #
29
4
  module Environment
30
- def self.setup(app, env: ENV["RACK_ENV"]) # :nodoc:
5
+ def self.setup(app, env: ENV["RACK_ENV"])
31
6
  app.set(:environment, (env || :development).to_sym)
32
7
  end
33
8
 
34
9
  module ClassMethods
35
- # Yields if current environment matches one of the given environments.
36
- #
37
- # class MyApp < Tynn
38
- # configure(:development, :staging) do
39
- # use(BetterErrors::Middleware)
40
- # end
41
- #
42
- # configure(:production) do
43
- # plugin(Tynn::SSL)
44
- # end
45
- # end
46
- #
47
10
  def configure(*envs)
48
11
  yield(self) if envs.include?(environment)
49
12
  end
50
13
 
51
- # Returns the current environment for the application.
52
- #
53
- # Tynn.environment
54
- # # => :development
55
- #
56
- # Tynn.set(environment, :test)
57
- #
58
- # Tynn.environment
59
- # # => :test
60
- #
61
14
  def environment
62
15
  settings[:environment]
63
16
  end
64
17
 
65
- # Checks if current environment is development. Returns <tt>true</tt> if
66
- # <tt>environment</tt> is <tt>:development</tt>. Otherwise, <tt>false</tt>.
67
- #
68
- # Tynn.set(:environment, :test)
69
- # Tynn.development? # => false
70
- #
71
- # Tynn.set(:environment, :development)
72
- # Tynn.development? # => true
73
- #
74
18
  def development?
75
19
  environment == :development
76
20
  end
77
21
 
78
- # Checks if current environment is test. Returns <tt>true</tt> if
79
- # <tt>environment</tt> is <tt>:test</tt>. Otherwise, <tt>false</tt>.
80
- #
81
- # Tynn.set(:environment, :development)
82
- # Tynn.test? # => false
83
- #
84
- # Tynn.set(:environment, :test)
85
- # Tynn.test? # => true
86
- #
87
22
  def test?
88
23
  environment == :test
89
24
  end
90
25
 
91
- # Checks if current environment is production. Returns <tt>true</tt> if
92
- # <tt>environment</tt> is <tt>:production</tt>. Otherwise, <tt>false</tt>.
93
- #
94
- # Tynn.set(:environment, :development)
95
- # Tynn.production? # => false
96
- #
97
- # Tynn.set(:environment, :production)
98
- # Tynn.production? # => true
99
- #
100
26
  def production?
101
27
  environment == :production
102
28
  end
103
29
 
104
- # Checks if current environment is staging. Returns <tt>true</tt> if
105
- # <tt>environment</tt> is <tt>:staging</tt>. Otherwise, <tt>false</tt>.
106
- #
107
- # Tynn.set(environment, :test)
108
- # Tynn.staging? # => false
109
- #
110
- # Tynn.set(:environment, :staging)
111
- # Tynn.staging? # => true
112
- #
113
30
  def staging?
114
31
  environment == :staging
115
32
  end
@@ -3,39 +3,38 @@
3
3
  require "json"
4
4
 
5
5
  class Tynn
6
- # Adds helper methods for JSON generation.
7
- #
8
- # require "tynn"
9
- # require "tynn/json"
10
- #
11
- # Tynn.plugin(Tynn::JSON)
12
- #
13
6
  module JSON
14
- def self.setup(app, options = {}) # :nodoc:
7
+ def self.setup(app, options = {})
15
8
  app.set(:json, {
16
- content_type: "application/json"
9
+ content_type: "application/json",
10
+ on_parse_error: proc { halt([400, {}, []]) },
11
+ read_opts: { create_additions: false },
12
+ write_opts: {}
17
13
  }.merge(options))
18
14
  end
19
15
 
20
16
  module InstanceMethods
21
- # Generates a JSON document from <tt>data</tt> and writes it to the
22
- # response body. It automatically sets the <tt>Content-Type</tt> header
23
- # to <tt>application/json</tt>.
24
- #
25
- # Tynn.define do
26
- # on "hash" do
27
- # json(foo: "bar")
28
- # end
29
- #
30
- # on "array" do
31
- # json([1, 2, 3])
32
- # end
33
- # end
34
- #
35
- def json(data)
17
+ def json_params
18
+ @__json_params ||= ::JSON.parse(req.body.read, json_opts[:read_opts])
19
+ rescue ::JSON::ParserError
20
+ rescue_json_parse_error
21
+ ensure
22
+ req.body.rewind
23
+ end
24
+
25
+ def rescue_json_parse_error
26
+ case (rescuer = json_opts[:on_parse_error])
27
+ when Symbol then send(rescuer)
28
+ when Proc then instance_eval(&rescuer)
29
+ end
30
+ end
31
+
32
+ private :rescue_json_parse_error
33
+
34
+ def json(data, opts = json_opts[:write_opts])
36
35
  res.content_type = json_opts[:content_type]
37
36
 
38
- res.write(::JSON.generate(data))
37
+ res.write(data.to_json(opts))
39
38
  end
40
39
 
41
40
  private
@@ -1,67 +1,106 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "tilt"
3
+ require_relative "utils"
4
4
 
5
5
  class Tynn
6
6
  module Render
7
- def self.setup(app, options = {}) # :nodoc:
7
+ def self.setup(app, options = {})
8
8
  app.set(:render, {
9
9
  content_type: "text/html",
10
- engine: "erb",
11
- engine_opts: { escape_html: true },
12
10
  layout: "layout",
13
11
  root: Dir.pwd,
14
12
  views: "views"
15
13
  }.merge(options))
16
14
  end
17
15
 
16
+ # Copyright (c) 2016 Francesco Rodriguez
17
+ # Copyright (c) 2011-2016 Michel Martens (https://github.com/soveran/mote)
18
+ #
19
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
20
+ # of this software and associated documentation files (the "Software"), to deal
21
+ # in the Software without restriction, including without limitation the rights
22
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
23
+ # copies of the Software, and to permit persons to whom the Software is
24
+ # furnished to do so, subject to the following conditions:
25
+ #
26
+ # The above copyright notice and this permission notice shall be included in
27
+ # all copies or substantial portions of the Software.
28
+ #
29
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
30
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
31
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
32
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
33
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
34
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
35
+ # THE SOFTWARE.
36
+
37
+ module Engine
38
+ PATTERN = /(<%)\s+(.*?)\s+%>(?:\n)|(<%==?)(.*?)%>/m
39
+
40
+ module_function
41
+
42
+ def parse(file, vars, context = self)
43
+ terms = File.read(file).split(PATTERN)
44
+ parts = "proc do |locals = {}, __o = ''|\n".dup
45
+
46
+ while var = vars.shift
47
+ parts << sprintf("%s = locals[%p]\n", var, var)
48
+ end
49
+
50
+ while term = terms.shift
51
+ parts << parse_expression(terms, term)
52
+ end
53
+
54
+ parts << "__o; end"
55
+
56
+ context.instance_eval(parts, file, -(vars.size.succ))
57
+ end
58
+
59
+ def parse_expression(terms, term)
60
+ case term
61
+ when "<%" then terms.shift << "\n"
62
+ when "<%=" then "__o << Tynn::Utils.h((" + terms.shift << ").to_s)\n"
63
+ when "<%==" then "__o << (" + terms.shift << ").to_s\n"
64
+ else "__o << " + term.dump << "\n"
65
+ end
66
+ end
67
+ end
68
+
18
69
  module InstanceMethods
19
- # Renders <tt>template</tt> within the default layout. An optional hash of
20
- # local variables can be passed to make available inside the template. It
21
- # automatically sets the <tt>Content-Type</tt> header to <tt>"text/html"</tt>.
22
- #
23
- # render("about", title: "About", name: "John Doe")
24
- #
25
- def render(template, locals = {})
26
- res.content_type = render_opts[:content_type]
27
-
28
- res.write(view(template, locals))
70
+ def render(template, content_type: nil, layout: render_layout, locals: {})
71
+ res.content_type = content_type || render_opts[:content_type]
72
+ res.write(view(template, layout: layout, locals: locals))
29
73
  end
30
74
 
31
- # Renders <tt>template</tt> within the default layout. An optional hash of
32
- # local variables can be passed to make available inside the template.
33
- #
34
- # res.write(view("about", title: "About", name: "John Doe"))
35
- #
36
- def view(template, locals = {})
37
- partial(render_opts[:layout], locals.merge(content: partial(template, locals)))
75
+ def view(template, layout: render_layout, locals: {})
76
+ partial(layout, locals: locals.merge(content: partial(template, locals: locals)))
38
77
  end
39
78
 
40
- # Renders <tt>template</tt> without a layout. An optional hash of local
41
- # variables can be passed to make available inside the template.
42
- #
43
- # res.write(partial("about", name: "John Doe"))
44
- #
45
- def partial(template, locals = {})
46
- tilt(template_path(template), locals.merge(app: self), render_opts[:engine_opts])
79
+ def partial(template, locals: {})
80
+ render_file(template_path(template), locals.merge(app: self))
47
81
  end
48
82
 
49
83
  private
50
84
 
51
85
  def template_path(template)
52
- File.join(views_path, "#{ template }.#{ render_opts[:engine] }")
86
+ File.join(views_path, "#{ template }.erb")
53
87
  end
54
88
 
55
89
  def views_path
56
90
  File.expand_path(render_opts[:views], render_opts[:root])
57
91
  end
58
92
 
59
- def tilt(file, locals = {}, opts = {})
60
- tilt_cache.fetch(file) { Tilt.new(file, 1, opts) }.render(self, locals)
93
+ def render_file(file, locals)
94
+ render_cache[file] ||= Engine.parse(file, locals.keys, TOPLEVEL_BINDING)
95
+ render_cache[file].call(locals)
96
+ end
97
+
98
+ def render_cache
99
+ Thread.current[:render_cache] ||= {}
61
100
  end
62
101
 
63
- def tilt_cache
64
- Thread.current[:tilt_cache] ||= Tilt::Cache.new
102
+ def render_layout
103
+ render_opts[:layout]
65
104
  end
66
105
 
67
106
  def render_opts