tynn 2.0.0.beta3 → 2.0.0.beta4

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