gin 1.0.4 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/gin/cache.rb ADDED
@@ -0,0 +1,65 @@
1
+ ##
2
+ # Gin::Cache is a bare-bones in-memory data store.
3
+ # It's thread-safe and built for read-bound data.
4
+ # Reads are lock-less when no writes are queued.
5
+
6
+ class Gin::Cache
7
+
8
+ def initialize
9
+ @data = {}
10
+ @lock = Gin::RWLock.new
11
+ end
12
+
13
+
14
+ ##
15
+ # Set the write timeout when waiting for reader thread locks.
16
+ # Defaults to 0.05 sec. See Gin::RWLock for more details.
17
+
18
+ def write_timeout= sec
19
+ @lock.write_timeout = sec
20
+ end
21
+
22
+
23
+ ##
24
+ # Get the write timeout when waiting for reader thread locks.
25
+ # See Gin::RWLock for more details.
26
+
27
+ def write_timeout
28
+ @lock.write_timeout
29
+ end
30
+
31
+
32
+ ##
33
+ # Get a value from the cache with the given key.
34
+
35
+ def [] key
36
+ @lock.read_sync{ @data[key] }
37
+ end
38
+
39
+
40
+ ##
41
+ # Set a value in the cache with the given key and value.
42
+
43
+ def []= key, val
44
+ @lock.write_sync{ @data[key] = val }
45
+ end
46
+
47
+
48
+ ##
49
+ # Check if the current key exists in the cache.
50
+
51
+ def has_key? key
52
+ @lock.read_sync{ @data.has_key? key }
53
+ end
54
+
55
+
56
+ ##
57
+ # Returns a cache value if it exists. Otherwise, locks and assigns the
58
+ # provided value or block result. Blocks get executed with the write lock
59
+ # to prevent redundant operations.
60
+
61
+ def cache key, value=nil, &block
62
+ return self[key] if self.has_key?(key)
63
+ @lock.write_sync{ @data[key] = block_given? ? yield() : value }
64
+ end
65
+ end
data/lib/gin/config.rb CHANGED
@@ -1,50 +1,206 @@
1
1
  require 'yaml'
2
2
 
3
+ ##
4
+ # Environment-specific config files loading mechanism.
5
+ #
6
+ # # config_dir/memcache.yml
7
+ # default: &default
8
+ # host: http://memcache.example.com
9
+ # connections: 5
10
+ #
11
+ # development: &dev
12
+ # host: localhost:123321
13
+ # connections: 1
14
+ #
15
+ # test: *dev
16
+ #
17
+ # staging:
18
+ # host: http://stage-memcache.example.com
19
+ #
20
+ # production: *default
21
+ #
22
+ #
23
+ # # config.rb
24
+ # config = Gin::Config.new 'staging', :dir => 'config/dir'
25
+ #
26
+ # config['memcache.host']
27
+ # #=> "http://stage-memcache.example.com"
28
+ #
29
+ # config['memcache.connections']
30
+ # #=> 5
31
+ #
32
+ # Config files get loaded on demand. They may also be reloaded on demand
33
+ # by setting the :ttl option to expire values. Values are only expired
34
+ # for configs with a source file whose mtime value differs from the one
35
+ # it had at its previous load time.
36
+ #
37
+ # # 5 minute expiration
38
+ # config = Gin::Config.new 'staging', :ttl => 300
39
+
3
40
  class Gin::Config
4
41
 
5
- attr_reader :dir
42
+ attr_accessor :dir, :logger, :ttl, :environment
43
+
44
+ ##
45
+ # Create a new config instance for the given environment name.
46
+ # The environment dictates which part of the config files gets exposed.
6
47
 
7
- def initialize environment, dir=nil
8
- self.dir = dir
48
+ def initialize environment, opts={}
9
49
  @environment = environment
10
- @meta = class << self; self; end
11
- @data = {}
12
- self.load!
50
+ @logger = opts[:logger] || $stdout
51
+ @ttl = opts[:ttl] || false
52
+ @dir = opts[:dir] || "./config"
53
+
54
+ @data = {}
55
+ @load_times = {}
56
+ @mtimes = {}
57
+
58
+ @lock = Gin::RWLock.new(opts[:write_timeout])
13
59
  end
14
60
 
15
61
 
16
- def dir= val
17
- @dir = File.join(val, "*.yml") if val
62
+ ##
63
+ # Get or set the write timeout when waiting for reader thread locks.
64
+ # Defaults to 0.05 sec. See Gin::RWLock for more details.
65
+
66
+ def write_timeout sec=nil
67
+ @lock.write_timeout = sec if sec
68
+ @lock.write_timeout
18
69
  end
19
70
 
20
71
 
72
+ ##
73
+ # Force-load all the config files in the config directory.
74
+
21
75
  def load!
22
76
  return unless @dir
23
- Dir[@dir].each do |filepath|
24
- c = YAML.load_file(filepath)
25
- c = (c['default'] || {}).merge (c[@environment] || {})
77
+ Dir[File.join(@dir, "*.yml")].each do |filepath|
78
+ load_config filepath
79
+ end
80
+ self
81
+ end
82
+
26
83
 
84
+ ##
85
+ # Load the given config name, or filename.
86
+ # # Loads @dir/my_config.yml
87
+ # config.load_config 'my_config'
88
+ # config['my_config']
89
+ # #=> data from file
90
+ #
91
+ # # Loads the given file if it exists.
92
+ # config.load_config 'path/to/my_config.yml'
93
+ # config['my_config']
94
+ # #=> data from file
95
+
96
+ def load_config name
97
+ name = name.to_s
98
+
99
+ if File.file?(name)
100
+ filepath = name
27
101
  name = File.basename(filepath, ".yml")
28
- set name, c
102
+ else
103
+ filepath = filepath_for(name)
29
104
  end
30
- self
105
+
106
+ raise Gin::MissingConfig, "No config file at #{filepath}" unless
107
+ File.file?(filepath)
108
+
109
+ @lock.write_sync do
110
+ @load_times[name] = Time.now
111
+
112
+ mtime = File.mtime(filepath)
113
+ return if mtime == @mtimes[name]
114
+
115
+ @mtimes[name] = mtime
116
+
117
+ c = YAML.load_file(filepath)
118
+ c = (c['default'] || {}).merge(c[@environment] || {})
119
+
120
+ @data[name] = c
121
+ end
122
+
123
+ rescue Psych::SyntaxError
124
+ @logger.write "[ERROR] Could not parse config `#{filepath}' as YAML"
125
+ return nil
31
126
  end
32
127
 
33
128
 
129
+ ##
130
+ # Sets a new config name and value. Configs set in this manner do not
131
+ # qualify for reloading as they don't have a source file.
132
+
34
133
  def set name, data
35
- @data[name] = data
36
- define_method(name){ @data[name] } unless respond_to? name
134
+ @lock.write_sync{ @data[name] = data }
37
135
  end
38
136
 
39
137
 
138
+ ##
139
+ # Get a config value from its name.
140
+ # Setting safe to true will return nil instead of raising errors.
141
+ # Reloads the config if reloading is enabled and value expired.
142
+
143
+ def get name, safe=false
144
+ return @lock.read_sync{ @data[name] } if
145
+ current?(name) || safe && !File.file?(filepath_for(name))
146
+
147
+ load_config(name) || @lock.read_sync{ @data[name] }
148
+ end
149
+
150
+
151
+ ##
152
+ # Checks if the given config is outdated.
153
+
154
+ def current? name
155
+ @lock.read_sync do
156
+ @ttl == false && @data.has_key?(name) ||
157
+ !@load_times[name] && @data.has_key?(name) ||
158
+ @load_times[name] && Time.now - @load_times[name] <= @ttl
159
+ end
160
+ end
161
+
162
+
163
+ ##
164
+ # Checks if a config exists in memory or on disk, by its name.
165
+ # # If foo config isn't loaded, looks for file under @dir/foo.yml
166
+ # config.has? 'foo'
167
+ # #=> true
168
+
40
169
  def has? name
41
- @data.has_key?(name) && respond_to?(name)
170
+ @lock.read_sync{ @data.has_key?(name) } || File.file?(filepath_for(name))
171
+ end
172
+
173
+
174
+ ##
175
+ # Non-raising config lookup. The following query the same value:
176
+ # # Raises an error if the 'user' key is missing.
177
+ # config.get('remote_shell')['user']['name']
178
+ #
179
+ # # Doesn't raise an error if a key is missing.
180
+ # # Doesn't support configs with '.' in the key names.
181
+ # config['remote_shell.user.name']
182
+
183
+ def [] key
184
+ chain = key.to_s.split(".")
185
+ name = chain.shift
186
+ curr = get(name, true)
187
+ return unless curr
188
+
189
+ chain.each do |k|
190
+ return unless Array === curr || Hash === curr
191
+ val = curr[k]
192
+ val = curr[k.to_i] if val.nil? && k.to_i.to_s == k
193
+ curr = val
194
+ end
195
+
196
+ curr
42
197
  end
43
198
 
44
199
 
45
200
  private
46
201
 
47
- def define_method name, &block
48
- @meta.send :define_method, name, &block
202
+
203
+ def filepath_for name
204
+ File.join(@dir, "#{name}.yml")
49
205
  end
50
206
  end
data/lib/gin/constants.rb CHANGED
@@ -38,10 +38,14 @@ module Gin::Constants
38
38
  GIN_RELOADED = 'gin.reloaded'.freeze
39
39
  GIN_ERRORS = 'gin.errors'.freeze
40
40
  GIN_TIMESTAMP = 'gin.timestamp'.freeze
41
+ GIN_TEMPLATES = 'gin.templates'.freeze
41
42
 
42
43
  # Environment names
43
44
  ENV_DEV = "development".freeze
44
45
  ENV_TEST = "test".freeze
45
46
  ENV_STAGE = "staging".freeze
46
47
  ENV_PROD = "production".freeze
48
+
49
+ # Other
50
+ SESSION_SECRET = "%064x" % Kernel.rand(2**256-1)
47
51
  end
@@ -1,3 +1,52 @@
1
+ # Gin controllers follow only a few rules:
2
+ #
3
+ # * ALL instance methods are actions (put your helper methods in a module or
4
+ # parent class not mounted to the app).
5
+ # * Filters are defined by blocks passed to special class methods.
6
+ # * Controller-level error handlers are defined by blocks passed to the 'error'
7
+ # class method.
8
+ #
9
+ # Gin controller actions are any instance method defined on the controller
10
+ # class. The HTTP response body takes the value of the method's return value.
11
+ #
12
+ # If the instance method takes arguments, matching params will be assigned to
13
+ # them. A required argument with a missing param triggers a Gin::BadRequest
14
+ # error, resulting in a 400 response.
15
+ #
16
+ # class UserController < Gin::Controller
17
+ # # Get params id and email
18
+ # def show id, email=nil
19
+ # ...
20
+ # end
21
+ # end
22
+ #
23
+ # Gin actions also support Ruby 2.0 keyed arguments, which are more flexible
24
+ # for assigning default values when dealing with missing params.
25
+ #
26
+ # class UserController < Gin::Controller
27
+ # def show(id, email: nil, full: false)
28
+ # ...
29
+ # end
30
+ # end
31
+ #
32
+ # Views are rendered by calling the 'view' method from a controller instance.
33
+ # Views don't halt the action but return a String of the rendered view.
34
+ # If a layout was set, the rendered view will include the layout.
35
+ #
36
+ # class UserController < Gin::Controller
37
+ # layout :user
38
+ #
39
+ # def show id
40
+ # # Renders <app.views_dir>/show_user.* with the layout <app.layouts_dir>/user.*
41
+ # view :show_user
42
+ # end
43
+ #
44
+ # def tos
45
+ # # Renders <app.views_dir>/tos.* with no layout
46
+ # view :tos, :layout => false
47
+ # end
48
+ # end
49
+
1
50
  class Gin::Controller
2
51
  extend GinClass
3
52
  include Gin::Constants
@@ -17,6 +66,20 @@ class Gin::Controller
17
66
  end
18
67
 
19
68
 
69
+ def self.inherited subclass
70
+ subclass.setup
71
+ super
72
+ end
73
+
74
+
75
+ def self.setup # :nodoc:
76
+ @layout = nil
77
+ @ctrl_name = Gin.underscore(self.to_s).gsub(/_?controller_?/,'')
78
+ end
79
+
80
+ setup
81
+
82
+
20
83
  ##
21
84
  # Array of action names for this controller.
22
85
 
@@ -31,8 +94,9 @@ class Gin::Controller
31
94
  # MyApp::FooController.controller_name
32
95
  # #=> "my_app/foo"
33
96
 
34
- def self.controller_name
35
- @ctrl_name ||= Gin.underscore(self.to_s).gsub(/_?controller_?/,'')
97
+ def self.controller_name new_name=nil
98
+ @ctrl_name = new_name if new_name
99
+ @ctrl_name
36
100
  end
37
101
 
38
102
 
@@ -41,10 +105,10 @@ class Gin::Controller
41
105
  # Default value is "text/html". This attribute is inherited.
42
106
 
43
107
  def self.content_type new_type=nil
44
- return @content_type = new_type if new_type
45
- return @content_type if @content_type
46
- self.superclass.respond_to?(:content_type) ?
47
- self.superclass.content_type.dup : "text/html"
108
+ @content_type = new_type if new_type
109
+ return @content_type if defined?(@content_type) && @content_type
110
+ self.superclass.respond_to?(:content_type) &&
111
+ self.superclass.content_type || "text/html"
48
112
  end
49
113
 
50
114
 
@@ -59,9 +123,36 @@ class Gin::Controller
59
123
  end
60
124
 
61
125
 
62
- class_proxy :controller_name
126
+ ##
127
+ # Get or set a layout for a given controller.
128
+ # Value can be a symbol or filepath.
129
+ # Layout file is expected to be in the Gin::App.layout_dir directory
130
+ # Defaults to the parent class layout, or Gin::App.layout.
131
+
132
+ def self.layout name=nil
133
+ @layout = name if name
134
+ return @layout if @layout
135
+ return self.superclass.layout if self.superclass.respond_to?(:layout)
136
+ end
137
+
138
+
139
+ class_rproxy :controller_name, :actions
140
+
141
+ # The Gin::App instance used by the controller. The App instance is meant for
142
+ # read-only use. Writes are not thread-safe and at your own risk.
143
+ attr_reader :app
144
+
145
+ # Gin::Request instance representing the HTTP request.
146
+ attr_reader :request
63
147
 
64
- attr_reader :app, :request, :response, :action, :env
148
+ # Gin::Response instance representing the HTTP response.
149
+ attr_reader :response
150
+
151
+ # The action that the HTTP request resolved to, based on the App's router.
152
+ attr_reader :action
153
+
154
+ # The Rack env hash.
155
+ attr_reader :env
65
156
 
66
157
 
67
158
  def initialize app, env
@@ -99,6 +190,14 @@ class Gin::Controller
99
190
  end
100
191
 
101
192
 
193
+ ##
194
+ # Accessor for @app.config.
195
+
196
+ def config
197
+ @app.config
198
+ end
199
+
200
+
102
201
  ##
103
202
  # Get the normalized mime-type matching the given input.
104
203
 
@@ -109,6 +208,9 @@ class Gin::Controller
109
208
 
110
209
  ##
111
210
  # Get or set the HTTP response Content-Type header.
211
+ # content_type :json
212
+ # content_type 'application/json;charset=us-ascii'
213
+ # content_type :json, charset: 'us-ascii'
112
214
 
113
215
  def content_type type=nil, params={}
114
216
  return @response[CNT_TYPE] unless type
@@ -296,6 +398,7 @@ class Gin::Controller
296
398
  # path_to :show, :id => 123
297
399
  # #=> "/foo/123"
298
400
  #
401
+ # # Default named route
299
402
  # path_to :show_foo, :id => 123
300
403
  # #=> "/foo/123"
301
404
 
@@ -516,6 +619,102 @@ class Gin::Controller
516
619
  end
517
620
 
518
621
 
622
+ ##
623
+ # Value of the layout to use for rendering.
624
+ # See also Gin::Controller.layout and Gin::App.layout.
625
+
626
+ def layout
627
+ self.class.layout || @app.layout
628
+ end
629
+
630
+
631
+ ##
632
+ # Returns the path to where the template is expected to be.
633
+ # template_path :foo
634
+ # #=> "<views_dir>/foo"
635
+ #
636
+ # template_path "sub/foo"
637
+ # #=> "<views_dir>/sub/foo"
638
+ #
639
+ # template_path "sub/foo", :layout
640
+ # #=> "<layouts_dir>/sub/foo"
641
+ #
642
+ # template_path "/other/foo"
643
+ # #=> "<root_dir>/other/foo"
644
+
645
+ def template_path template, is_layout=false
646
+ dir = if template[0] == ?/
647
+ @app.root_dir
648
+ elsif is_layout
649
+ @app.layouts_dir
650
+ else
651
+ @app.views_dir
652
+ end
653
+
654
+ path = File.join(dir, template.to_s)
655
+ path.gsub!('*', controller_name)
656
+ File.expand_path(path)
657
+ end
658
+
659
+
660
+ ##
661
+ # Render a template with the given view template.
662
+ # Options supported:
663
+ # :locals:: Hash - local variables used in template
664
+ # :layout:: Symbol/String - a custom layout to use
665
+ # :scope:: Object - The scope in which to render the template: default self
666
+ # :content_type:: Symbol/String - Content-Type header to set
667
+ # :engine:: String - Tilt rendering engine to use
668
+ # :layout_engine:: String - Tilt layout rendering engine to use
669
+ #
670
+ # The template argument may be a String or a Symbol. By default the
671
+ # template location will be looked for under Gin::App.views_dir, but
672
+ # the directory may be specified as any directory under Gin::App.root_dir
673
+ # by using the '/' prefix:
674
+ #
675
+ # view 'foo/template'
676
+ # #=> Renders file "<views_dir>/foo/template"
677
+ #
678
+ # view '/foo/template'
679
+ # #=> Renders file "<root_dir>/foo/template"
680
+ #
681
+ # # Render without layout
682
+ # view 'foo/template', layout: false
683
+
684
+ def view template, opts={}, &block
685
+ content_type(opts.delete(:content_type)) if opts[:content_type]
686
+
687
+ scope = opts[:scope] || self
688
+ locals = opts[:locals] || {}
689
+
690
+ template = template_path(template)
691
+ v_template = @app.template_for template, opts[:engine]
692
+ raise Gin::TemplateMissing, "No such template `#{template}'" unless v_template
693
+
694
+ if opts[:layout] != false
695
+ r_layout = template_path((opts[:layout] || layout), true)
696
+ r_template = @app.template_for r_layout, opts[:layout_engine] if r_layout
697
+ end
698
+
699
+ if !@response[CNT_TYPE]
700
+ mime_type = v_template.class.default_mime_type ||
701
+ r_template && r_template.class.default_mime_type
702
+ content_type(mime_type) if mime_type
703
+ end
704
+
705
+ @env[GIN_TEMPLATES] ||= []
706
+
707
+ if r_template
708
+ @env[GIN_TEMPLATES] << r_template.file << v_template.file
709
+ r_template.render(scope, locals){
710
+ v_template.render(scope, locals, &block) }
711
+ else
712
+ @env[GIN_TEMPLATES] << v_template.file
713
+ v_template.render(scope, locals, &block)
714
+ end
715
+ end
716
+
717
+
519
718
  ##
520
719
  # Taken from Sinatra.
521
720
  #
@@ -567,12 +766,13 @@ class Gin::Controller
567
766
  def html_error_page err, code=nil
568
767
  if @app.development?
569
768
  fulltrace = err.backtrace.join("\n")
570
- fulltrace = "<pre>#{fulltrace}</pre>"
769
+ fulltrace = "<pre>#{h(fulltrace)}</pre>"
571
770
 
572
771
  apptrace = Gin.app_trace(err.backtrace).join("\n")
573
- apptrace = "<pre>#{apptrace}</pre>" unless apptrace.empty?
772
+ apptrace = "<pre>#{h(apptrace)}</pre>" unless apptrace.empty?
574
773
 
575
- DEV_ERROR_HTML % [err.class, err.class, err.message, apptrace, fulltrace]
774
+ DEV_ERROR_HTML %
775
+ [h(err.class), h(err.class), h(err.message), apptrace, fulltrace]
576
776
 
577
777
  else
578
778
  code ||= status
@@ -588,6 +788,14 @@ class Gin::Controller
588
788
  end
589
789
 
590
790
 
791
+ ##
792
+ # HTML-escape the given String.
793
+
794
+ def h obj
795
+ CGI.escapeHTML obj.to_s
796
+ end
797
+
798
+
591
799
  private
592
800
 
593
801