gin 1.0.4 → 1.1.0

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