crystal_ext 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +66 -0
- data/lib/crystal_ext/defer_js.rb +73 -0
- data/lib/crystal_ext/ensure_no_www.rb +37 -0
- data/lib/crystal_ext/i18n.rb +23 -0
- data/lib/crystal_ext/i18n/locales/ru/pluralization.rb +62 -0
- data/lib/crystal_ext/plugin.rb +21 -0
- data/lib/crystal_ext/plugin/app.rb +19 -0
- data/lib/crystal_ext/plugin/web.rb +44 -0
- data/lib/crystal_ext/prepare_model.rb +18 -0
- data/lib/crystal_ext/profiles/web_ext.rb +75 -0
- data/lib/crystal_ext/profiles/web_ext_require.rb +13 -0
- data/lib/crystal_ext/protect_from_forgery.rb +64 -0
- data/lib/crystal_ext/user_error.rb +12 -0
- data/readme.md +14 -0
- data/spec/controller_helper.rb +19 -0
- data/spec/defer_js_spec.rb +32 -0
- data/spec/helper.rb +5 -0
- data/spec/i18n_spec.rb +30 -0
- data/spec/i18n_spec/locales/en/general.yml +5 -0
- data/spec/i18n_spec/locales/ru/general.yml +7 -0
- data/spec/prepare_model_spec.rb +42 -0
- data/spec/protect_from_forgery_spec.rb +169 -0
- data/spec/spec.opts +4 -0
- data/spec/user_error_spec.rb +41 -0
- metadata +103 -0
data/Rakefile
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'fileutils'
|
3
|
+
current_dir = File.expand_path(File.dirname(__FILE__))
|
4
|
+
Dir.chdir current_dir
|
5
|
+
|
6
|
+
|
7
|
+
#
|
8
|
+
# Specs
|
9
|
+
#
|
10
|
+
require 'spec/rake/spectask'
|
11
|
+
|
12
|
+
task :default => :spec
|
13
|
+
|
14
|
+
Spec::Rake::SpecTask.new('spec') do |t|
|
15
|
+
t.spec_files = FileList["spec/**/*_spec.rb"].select{|f| f !~ /\/_/}
|
16
|
+
t.libs = ["#{current_dir}/lib"]
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
#
|
21
|
+
# Gem
|
22
|
+
#
|
23
|
+
require 'rake/clean'
|
24
|
+
require 'rake/gempackagetask'
|
25
|
+
|
26
|
+
gem_options = {
|
27
|
+
:name => "crystal_ext",
|
28
|
+
:version => "0.0.4",
|
29
|
+
:summary => "Extensions for the Crystal Framework",
|
30
|
+
:dependencies => %w(crystal)
|
31
|
+
}
|
32
|
+
|
33
|
+
gem_name = gem_options[:name]
|
34
|
+
spec = Gem::Specification.new do |s|
|
35
|
+
gem_options.delete(:dependencies).each{|d| s.add_dependency d}
|
36
|
+
gem_options.each{|k, v| s.send "#{k}=", v}
|
37
|
+
|
38
|
+
s.name = gem_name
|
39
|
+
s.author = "Alexey Petrushin"
|
40
|
+
s.homepage = "http://github.com/alexeypetrushin/#{gem_options[:name]}"
|
41
|
+
s.require_path = "lib"
|
42
|
+
s.files = (%w{Rakefile readme.md} + Dir.glob("{lib,spec}/**/*"))
|
43
|
+
|
44
|
+
s.platform = Gem::Platform::RUBY
|
45
|
+
s.has_rdoc = true
|
46
|
+
end
|
47
|
+
|
48
|
+
package_dir = "#{current_dir}/build"
|
49
|
+
Rake::GemPackageTask.new(spec) do |p|
|
50
|
+
p.need_tar = true if RUBY_PLATFORM !~ /mswin/
|
51
|
+
p.need_zip = true
|
52
|
+
p.package_dir = package_dir
|
53
|
+
end
|
54
|
+
|
55
|
+
task :push do
|
56
|
+
# dir = Dir.chdir package_dir do
|
57
|
+
gem_file = Dir.glob("#{package_dir}/#{gem_name}*.gem").first
|
58
|
+
system "gem push #{gem_file}"
|
59
|
+
# end
|
60
|
+
end
|
61
|
+
|
62
|
+
task :clean do
|
63
|
+
system "rm -r #{package_dir}"
|
64
|
+
end
|
65
|
+
|
66
|
+
task :release => [:gem, :push, :clean]
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Crystal
|
2
|
+
JavascriptHelper.class_eval do
|
3
|
+
|
4
|
+
def javascript_tag_with_defer value = nil, &block
|
5
|
+
if @defer_js and !@deferred_js_called
|
6
|
+
@deffered_js_initialized.must_be.true
|
7
|
+
|
8
|
+
value = capture &block if block
|
9
|
+
|
10
|
+
value = <<END
|
11
|
+
deferred_static_scripts.push(function(){
|
12
|
+
#{value}
|
13
|
+
});
|
14
|
+
END
|
15
|
+
|
16
|
+
value = javascript_tag_without_defer value
|
17
|
+
if block
|
18
|
+
concat value
|
19
|
+
else
|
20
|
+
value
|
21
|
+
end
|
22
|
+
else
|
23
|
+
javascript_tag_without_defer value, &block
|
24
|
+
end
|
25
|
+
end
|
26
|
+
alias_method_chain :javascript_tag, :defer
|
27
|
+
|
28
|
+
def initialize_deferred_js
|
29
|
+
return "" unless @defer_js
|
30
|
+
|
31
|
+
@deffered_js_initialized.must_be.false
|
32
|
+
@deffered_js_initialized = true
|
33
|
+
dont_defer_js do
|
34
|
+
javascript_tag "var deferred_static_scripts = [];"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def call_deferred_js
|
39
|
+
return "" unless @defer_js
|
40
|
+
|
41
|
+
@deffered_js_initialized.must_be.true
|
42
|
+
@deferred_js_called.must_be.false
|
43
|
+
@deferred_js_called = true
|
44
|
+
content = <<END
|
45
|
+
var i;
|
46
|
+
for(i in deferred_static_scripts){deferred_static_scripts[i]()}
|
47
|
+
deferred_static_scripts = [];
|
48
|
+
END
|
49
|
+
dont_defer_js do
|
50
|
+
javascript_tag content
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def dont_defer_js &block
|
55
|
+
before = @defer_js
|
56
|
+
begin
|
57
|
+
@defer_js = false
|
58
|
+
block.call
|
59
|
+
ensure
|
60
|
+
@defer_js = before
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def defer_js!
|
65
|
+
@defer_js.must_be.false
|
66
|
+
workspace.params.format.must == 'html'
|
67
|
+
|
68
|
+
@defer_js = true
|
69
|
+
@deferred_js_called = false
|
70
|
+
# @defer_static_scripts = workspace.params.format.js? ? false : true
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# enshure domain has no www. except if there's custom subdomain
|
2
|
+
module Crystal
|
3
|
+
# class UrlWithWww < StandardError
|
4
|
+
# end
|
5
|
+
|
6
|
+
module Processors
|
7
|
+
class EnsureNoWww < Processor
|
8
|
+
def call
|
9
|
+
workspace.params.must_be.defined
|
10
|
+
if workspace.params.html? and url_with_www?
|
11
|
+
redirect_without_www
|
12
|
+
else
|
13
|
+
next_processor.call
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
def uri
|
19
|
+
@uri ||= Uri.parse workspace.request.url
|
20
|
+
end
|
21
|
+
|
22
|
+
def url_with_www?
|
23
|
+
uri.host =~ /^www\./
|
24
|
+
end
|
25
|
+
|
26
|
+
def redirect_without_www
|
27
|
+
uri.host = uri.host.sub(/^www\./, '')
|
28
|
+
url = uri.to_s
|
29
|
+
|
30
|
+
response = workspace.response
|
31
|
+
response.status = 301
|
32
|
+
response.headers['Location'] = url
|
33
|
+
response.body = %(<html><body>You are being <a href="#{url.html_escape}">redirected</a>.</body></html>)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# Hack, ActiveSupport currently uses differrent version
|
2
|
+
# gem 'i18n', '>= 0.4.1'
|
3
|
+
# require 'i18n'
|
4
|
+
|
5
|
+
require "i18n/backend/pluralization"
|
6
|
+
I18n::Backend::Simple.send(:include, I18n::Backend::Pluralization)
|
7
|
+
|
8
|
+
dir = File.expand_path(File.dirname(__FILE__))
|
9
|
+
I18n.load_path += Dir["#{dir}/i18n/locales/*/*.{rb,yml}"]
|
10
|
+
|
11
|
+
|
12
|
+
#
|
13
|
+
# Helpers for Crystal
|
14
|
+
#
|
15
|
+
[
|
16
|
+
:AbstractController,
|
17
|
+
:ControllerContext,
|
18
|
+
].each do |klass_name|
|
19
|
+
if defined?(Crystal) and Crystal.const_defined?(klass_name)
|
20
|
+
klass = Crystal.const_get(klass_name)
|
21
|
+
klass.delegate :t, :to => I18n
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# {
|
2
|
+
# :'ru' => {
|
3
|
+
# :pluralize => lambda { |n|
|
4
|
+
# # Правило плюрализации для русского языка, взято из CLDR, http://unicode.org/cldr/
|
5
|
+
# #
|
6
|
+
# #
|
7
|
+
# # Russian language pluralization rules, taken from CLDR project, http://unicode.org/cldr/
|
8
|
+
# #
|
9
|
+
# # one -> n mod 10 is 1 and n mod 100 is not 11;
|
10
|
+
# # few -> n mod 10 in 2..4 and n mod 100 not in 12..14;
|
11
|
+
# # many -> n mod 10 is 0 or n mod 10 in 5..9 or n mod 100 in 11..14;
|
12
|
+
# # other -> everything else
|
13
|
+
# #
|
14
|
+
# # Пример
|
15
|
+
# #
|
16
|
+
# # :one = 1, 21, 31, 41, 51, 61...
|
17
|
+
# # :few = 2-4, 22-24, 32-34...
|
18
|
+
# # :many = 0, 5-20, 25-30, 35-40...
|
19
|
+
# # :other = 1.31, 2.31, 5.31...
|
20
|
+
# modulo10 = n.modulo(10)
|
21
|
+
# modulo100 = n.modulo(100)
|
22
|
+
#
|
23
|
+
# if modulo10 == 1 && modulo100 != 11
|
24
|
+
# :one
|
25
|
+
# elsif (modulo10 == 2 || modulo10 == 3 || modulo10 == 4) && !(modulo100 == 12 || modulo100 == 13 || modulo100 == 14)
|
26
|
+
# :few
|
27
|
+
# elsif modulo10 == 0 || (modulo10 == 5 || modulo10 == 6 || modulo10 == 7 || modulo10 == 8 || modulo10 == 9) || (modulo100 == 11 || modulo100 == 12 || modulo100 == 13 || modulo100 == 14)
|
28
|
+
# :many
|
29
|
+
# else
|
30
|
+
# :other
|
31
|
+
# end
|
32
|
+
# }
|
33
|
+
# }
|
34
|
+
# }
|
35
|
+
|
36
|
+
{
|
37
|
+
:ru => {
|
38
|
+
:i18n => {
|
39
|
+
:plural => {
|
40
|
+
:rule => lambda{|n|
|
41
|
+
# Правило плюрализации для русского языка, взято из CLDR, http://unicode.org/cldr/
|
42
|
+
#
|
43
|
+
#
|
44
|
+
# Russian language pluralization rules, taken from CLDR project, http://unicode.org/cldr/
|
45
|
+
#
|
46
|
+
# one -> n mod 10 is 1 and n mod 100 is not 11;
|
47
|
+
# few -> n mod 10 in 2..4 and n mod 100 not in 12..14;
|
48
|
+
# many -> n mod 10 is 0 or n mod 10 in 5..9 or n mod 100 in 11..14;
|
49
|
+
# other -> everything else
|
50
|
+
#
|
51
|
+
# Пример
|
52
|
+
#
|
53
|
+
# :one = 1, 21, 31, 41, 51, 61...
|
54
|
+
# :few = 2-4, 22-24, 32-34...
|
55
|
+
# :many = 0, 5-20, 25-30, 35-40...
|
56
|
+
# :other = 1.31, 2.31, 5.31...
|
57
|
+
n % 10 == 1 && n % 100 != 11 ? :one : [2, 3, 4].include?(n % 10) && ![12, 13, 14].include?(n % 100) ? :few : n % 10 == 0 || [5, 6, 7, 8, 9].include?(n % 10) || [11, 12, 13, 14].include?(n % 100) ? :many : :other
|
58
|
+
}
|
59
|
+
}
|
60
|
+
}
|
61
|
+
}
|
62
|
+
}
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Crystal
|
2
|
+
@plugin_initializers = {}
|
3
|
+
class << self
|
4
|
+
attr_accessor :plugin_initializers
|
5
|
+
|
6
|
+
def initialize_plugin type, dir, *args, &block
|
7
|
+
type = type.to_s
|
8
|
+
plugin_initializers.must.include(type)
|
9
|
+
|
10
|
+
pi_class = plugin_initializers[type]
|
11
|
+
pi = pi_class.new dir, *args
|
12
|
+
block.call pi if block
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
require 'crystal_ext/plugin/web'
|
18
|
+
crystal.plugin_initializers['web'] = Crystal::Plugin::Web
|
19
|
+
|
20
|
+
require 'crystal_ext/plugin/app'
|
21
|
+
crystal.plugin_initializers['app'] = Crystal::Plugin::App
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Crystal
|
2
|
+
module Plugin
|
3
|
+
class App
|
4
|
+
attr_reader :dir
|
5
|
+
|
6
|
+
def initialize dir
|
7
|
+
@dir = dir
|
8
|
+
end
|
9
|
+
|
10
|
+
def plugins list
|
11
|
+
plugins = Array.wrap(list)
|
12
|
+
# add to $LOAD_PATH
|
13
|
+
# plugins.each{|plugin| $LOAD_PATH << "#{dir}/#{plugin}" unless $LOAD_PATH.include?("#{dir}/#{plugin}")}
|
14
|
+
# call plugin's init
|
15
|
+
plugins.each{|plugin| require "#{plugin}/config/init" if File.exist? "#{dir}/#{plugin}/config/init.rb"}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Crystal
|
2
|
+
module Plugin
|
3
|
+
class Web
|
4
|
+
attr_reader :dir
|
5
|
+
|
6
|
+
def initialize dir
|
7
|
+
@dir = dir
|
8
|
+
end
|
9
|
+
|
10
|
+
def asset name
|
11
|
+
require 'asset_packager'
|
12
|
+
AssetPackager.add "#{dir}/config/asset_packages.yml", "#{dir}/public"
|
13
|
+
|
14
|
+
crystal.after :environment do
|
15
|
+
crystal.ensure_public_symlink name, "#{dir}/public"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def routes
|
20
|
+
crystal.after :environment do
|
21
|
+
routes_file = "#{dir}/config/routes.rb"
|
22
|
+
load routes_file if File.exist? routes_file
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def locales
|
27
|
+
I18n.load_path += Dir["#{dir}/config/locales/**/*.{rb,yml}"]
|
28
|
+
end
|
29
|
+
|
30
|
+
def require_paths *relative_paths
|
31
|
+
relative_paths = relative_paths.first if relative_paths.first.is_a? Array
|
32
|
+
relative_paths.each do |relative_path|
|
33
|
+
path = "#{dir}/#{relative_path}"
|
34
|
+
$LOAD_PATH << path unless $LOAD_PATH.include? path
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def autoload *list
|
39
|
+
list = list.first if list.first.is_a? Array
|
40
|
+
list.each{|d| autoload_dir "#{dir}/#{d}"}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
Crystal::AbstractController::ClassMethods.class_eval do
|
2
|
+
def prepare_model aclass, opt = {}
|
3
|
+
id = opt.delete(:id) || :id
|
4
|
+
|
5
|
+
# variable = opt.delete(:variable) || aclass.model_name.underscore
|
6
|
+
opt.must.include :variable
|
7
|
+
variable = opt.delete(:variable)
|
8
|
+
|
9
|
+
finder = opt.delete(:finder) || :find!
|
10
|
+
|
11
|
+
method = "prepare_#{variable}"
|
12
|
+
define_method method do
|
13
|
+
model = aclass.send finder, params[id]
|
14
|
+
instance_variable_set "@#{variable}", model
|
15
|
+
end
|
16
|
+
before method, opt
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'crystal_ext/profiles/web_ext_require'
|
2
|
+
|
3
|
+
#
|
4
|
+
# Routes
|
5
|
+
#
|
6
|
+
Crystal::HttpController.include Crystal::NamedRouter::Helper
|
7
|
+
Crystal::ControllerContext.include Crystal::NamedRouter::Helper
|
8
|
+
|
9
|
+
crystal.after :environment do
|
10
|
+
crystal.register(:router, :depends_on => :environment) do
|
11
|
+
Crystal::Router.new(:class, [
|
12
|
+
[:named_router, Crystal::NamedRouter.new],
|
13
|
+
[:default_router, Crystal::DefaultRouter.new]
|
14
|
+
])
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
#
|
20
|
+
# Conveyors
|
21
|
+
#
|
22
|
+
module Crystal::Processors
|
23
|
+
crystal.after :environment do
|
24
|
+
crystal.conveyors.web do |web|
|
25
|
+
# conveyor
|
26
|
+
web.use ConveyorLogger
|
27
|
+
|
28
|
+
# http
|
29
|
+
web.use HttpWriter, :content
|
30
|
+
web.use PrepareParams
|
31
|
+
web.use EvaluateFormat
|
32
|
+
web.use HTTPLogger
|
33
|
+
|
34
|
+
# forgery protection
|
35
|
+
web.use PrepareAutenticityToken
|
36
|
+
|
37
|
+
# ensure no www
|
38
|
+
web.use EnsureNoWww
|
39
|
+
|
40
|
+
# html
|
41
|
+
web.use ScopedParams
|
42
|
+
|
43
|
+
# controller
|
44
|
+
web.use ControllerErrorHandling, :content
|
45
|
+
|
46
|
+
# router
|
47
|
+
web.use Router, :class, :method_name
|
48
|
+
|
49
|
+
# controller
|
50
|
+
web.use ControllerLogger
|
51
|
+
web.use ControllerCaller, :content
|
52
|
+
|
53
|
+
web.build!
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
#
|
60
|
+
# RackAdapter
|
61
|
+
#
|
62
|
+
Crystal::RackAdapter.build_common_app do |builder|
|
63
|
+
config = crystal.config
|
64
|
+
|
65
|
+
# CommonLogger, ShowExceptions, Lint
|
66
|
+
builder.use Rack::Lint if config.development?
|
67
|
+
builder.use Crystal::StaticFiles, "#{config.root!}/public" if config.static? and config.root? and config.development?
|
68
|
+
|
69
|
+
# use Rack::Session::Cookie, :key => 'rack.session', :domain => 'foo.com', :path => '/', :expire_after => 2592000, :secret => 'change_me'
|
70
|
+
builder.use Rack::Session::Cookie, config.session.to_hash if config.session?
|
71
|
+
|
72
|
+
# builder.use Rack::CommonLogger
|
73
|
+
builder.use Rack::MethodOverride
|
74
|
+
# builder.use ShowExceptions if config.show_exceptions?
|
75
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
#
|
2
|
+
# Processor
|
3
|
+
#
|
4
|
+
module Crystal
|
5
|
+
module Processors
|
6
|
+
class PrepareAutenticityToken < Processor
|
7
|
+
def call
|
8
|
+
if config.session?
|
9
|
+
request = workspace.request.must_be.defined
|
10
|
+
params = workspace.params.must_be.defined
|
11
|
+
|
12
|
+
token = request.session['authenticity_token'] || params['session_authenticity_token']
|
13
|
+
|
14
|
+
if token.blank? and request.get? and
|
15
|
+
token = generate_authenticity_token
|
16
|
+
request.session['authenticity_token'] = token
|
17
|
+
end
|
18
|
+
|
19
|
+
workspace.session_authenticity_token = token
|
20
|
+
end
|
21
|
+
|
22
|
+
next_processor.call
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
protected
|
27
|
+
def generate_authenticity_token
|
28
|
+
ActiveSupport::SecureRandom.base64(32)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
#
|
36
|
+
# Remote
|
37
|
+
#
|
38
|
+
Crystal::HttpController.class_eval do
|
39
|
+
BROWSER_GENERATED_TYPES = %w(html url_encoded_form multipart_form text).to_set
|
40
|
+
|
41
|
+
protected
|
42
|
+
def protect_from_forgery &block
|
43
|
+
request = workspace.request
|
44
|
+
allow = (
|
45
|
+
request.get? or
|
46
|
+
# TODO2 check content_type is lovercased
|
47
|
+
!BROWSER_GENERATED_TYPES.include?(request.content_type) or
|
48
|
+
(workspace.session_authenticity_token.present? and
|
49
|
+
workspace.session_authenticity_token == params.authenticity_token)
|
50
|
+
)
|
51
|
+
|
52
|
+
if allow
|
53
|
+
block.call
|
54
|
+
else
|
55
|
+
raise "Invalid authenticity token!"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
Crystal::HttpController::ClassMethods.class_eval do
|
61
|
+
def protect_from_forgery options = {}
|
62
|
+
around :protect_from_forgery, options
|
63
|
+
end
|
64
|
+
end
|
data/readme.md
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# Extensions for the Crystal framework.
|
2
|
+
|
3
|
+
- abstract_interface - tools for rapid interface creation.
|
4
|
+
- common_interface - concrete interface built with abstract_interface tools.
|
5
|
+
- asset_packager - packages CSS and JS asset for your application.
|
6
|
+
- crystal_ext - additions and extensions of the Crystal.
|
7
|
+
- crystal_jquery - jQuery plugin for the Crystal.
|
8
|
+
- mongo_mapper_ext - additions and extensions of the MongoMapper.
|
9
|
+
- rad - Rapid Application Development tool built upon of the Crystal.
|
10
|
+
|
11
|
+
Please go to specs for docs.
|
12
|
+
|
13
|
+
P.S.
|
14
|
+
All these components will be in standalone gems, but right now it's simpler for me to hold all of them in one place.
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require "#{File.expand_path(File.dirname(__FILE__))}/helper"
|
2
|
+
|
3
|
+
require 'crystal/profiles/web_require'
|
4
|
+
|
5
|
+
require 'crystal/spec'
|
6
|
+
|
7
|
+
Spec::Example::ExampleGroup.class_eval do
|
8
|
+
def self.with_controller_ext_spec
|
9
|
+
before :all do
|
10
|
+
crystal.after :environment do
|
11
|
+
crystal.register(:router, :depends_on => :environment){Crystal::Router.new(:class, [Crystal::DefaultRoute.new])}
|
12
|
+
|
13
|
+
crystal.conveyors.web do |web|
|
14
|
+
web.use Crystal::Processors::ControllerCaller, :content
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require "#{File.expand_path(File.dirname(__FILE__))}/controller_helper"
|
2
|
+
require 'crystal/html'
|
3
|
+
require 'crystal_ext/defer_js'
|
4
|
+
|
5
|
+
describe "Defer JS" do
|
6
|
+
before :each do
|
7
|
+
class ContextStub
|
8
|
+
include Crystal::BasicHtmlHelper, Crystal::JavascriptHelper
|
9
|
+
|
10
|
+
def workspace
|
11
|
+
@workspace ||= {
|
12
|
+
:params => {
|
13
|
+
:format => 'html'
|
14
|
+
}.to_openobject
|
15
|
+
}.to_openobject
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
@t = ContextStub.new
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should work as usual if deffered mode not enabled" do
|
23
|
+
@t.javascript_tag("script").should == %(<script type="text/javascript">script</script>)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "deffered mode" do
|
27
|
+
@t.defer_js!
|
28
|
+
@t.initialize_deferred_js.should =~ /var deferred_static_scripts/
|
29
|
+
@t.javascript_tag('script').should =~ /deferred_static_scripts/
|
30
|
+
@t.call_deferred_js.should =~ /deferred_static_scripts/
|
31
|
+
end
|
32
|
+
end
|
data/spec/helper.rb
ADDED
data/spec/i18n_spec.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require "#{File.expand_path(File.dirname(__FILE__))}/helper"
|
2
|
+
require 'crystal/support' # to check that i18n will correctly works with ActiveSupport
|
3
|
+
require 'crystal_ext/i18n'
|
4
|
+
|
5
|
+
describe 'I18n' do
|
6
|
+
before :all do
|
7
|
+
dir = File.expand_path(File.dirname(__FILE__))
|
8
|
+
I18n.load_path += Dir["#{dir}/i18n_spec/locales/*/*.{rb,yml}"]
|
9
|
+
end
|
10
|
+
|
11
|
+
def t *args
|
12
|
+
I18n.t *args
|
13
|
+
end
|
14
|
+
|
15
|
+
it "basic" do
|
16
|
+
I18n.locale = 'en'
|
17
|
+
t(:name).should == "Name"
|
18
|
+
t(:name).is_a?(String).should be_true
|
19
|
+
|
20
|
+
I18n.locale = 'ru'
|
21
|
+
t(:name).should == "Имя"
|
22
|
+
end
|
23
|
+
|
24
|
+
it "pluggable pluralization" do
|
25
|
+
I18n.locale = 'ru'
|
26
|
+
t(:comments_count, :count => 1).should == "1 комментарий"
|
27
|
+
t(:comments_count, :count => 2).should == "2 комментария"
|
28
|
+
t(:comments_count, :count => 5).should == "5 комментариев"
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require "#{File.expand_path(File.dirname(__FILE__))}/controller_helper"
|
2
|
+
require 'crystal_ext/prepare_model'
|
3
|
+
|
4
|
+
describe "User Error" do
|
5
|
+
with_environment :test
|
6
|
+
with_controller_ext_spec
|
7
|
+
|
8
|
+
before :all do
|
9
|
+
class ::SomeModel
|
10
|
+
def self.find! id
|
11
|
+
id.should == 'some id'
|
12
|
+
SomeModel.new
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# crystal[:config].environment = :test
|
17
|
+
# crystal[:environment] = Crystal::Environment.new
|
18
|
+
# crystal[:environment].load :controller_ext_spec
|
19
|
+
end
|
20
|
+
|
21
|
+
after :all do
|
22
|
+
remove_constants %w(SomeModel ControllerSpec)
|
23
|
+
|
24
|
+
# Micon.clear
|
25
|
+
end
|
26
|
+
|
27
|
+
it "user error" do
|
28
|
+
class ::ControllerSpec
|
29
|
+
inherit Crystal::HttpController
|
30
|
+
|
31
|
+
prepare_model SomeModel, :id => :some_model, :variable => 'some_model'
|
32
|
+
|
33
|
+
def action
|
34
|
+
@some_model.should_not == nil
|
35
|
+
render :inline => 'ok'
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
ccall(ControllerSpec, :action, :some_model => 'some id').content.should == 'ok'
|
40
|
+
# workspace.response.should == [200, {"Content-Type"=>"application/json"}, %({"result":true})]
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
require "#{File.expand_path(File.dirname(__FILE__))}/helper"
|
2
|
+
|
3
|
+
require 'crystal/profiles/web_require'
|
4
|
+
require 'crystal_ext/protect_from_forgery'
|
5
|
+
|
6
|
+
require 'crystal/spec/environment'
|
7
|
+
|
8
|
+
describe "Forgery protection" do
|
9
|
+
with_environment :test
|
10
|
+
|
11
|
+
before :all do
|
12
|
+
class ForgerySpecHelper < Crystal::Processor
|
13
|
+
def call
|
14
|
+
block = workspace.check_forgery.before_request
|
15
|
+
block.call workspace if block
|
16
|
+
workspace.before_request_done = true
|
17
|
+
|
18
|
+
next_processor.call
|
19
|
+
|
20
|
+
block = workspace.check_forgery.after_request
|
21
|
+
block.call workspace if block
|
22
|
+
workspace.after_request_done = true
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class ::AnRemote
|
27
|
+
inherit Crystal::HttpController
|
28
|
+
|
29
|
+
protect_from_forgery :only => :protected_method
|
30
|
+
|
31
|
+
def protected_method
|
32
|
+
render :inline => 'protected result'
|
33
|
+
end
|
34
|
+
|
35
|
+
def method_without_protection
|
36
|
+
render :inline => 'result'
|
37
|
+
end
|
38
|
+
|
39
|
+
def dumb_method; end
|
40
|
+
end
|
41
|
+
|
42
|
+
crystal.after :environment do
|
43
|
+
crystal.conveyors.web do |web|
|
44
|
+
web.use Crystal::Processors::PrepareParams
|
45
|
+
web.use Crystal::Processors::EvaluateFormat
|
46
|
+
web.use ForgerySpecHelper
|
47
|
+
web.use Crystal::Processors::PrepareAutenticityToken
|
48
|
+
web.use Crystal::Processors::ControllerCaller, :content
|
49
|
+
end
|
50
|
+
|
51
|
+
crystal.config.session = {'key' => 'session_id'}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
after :all do
|
56
|
+
remove_constants %w(AnRemote ForgerySpecHelper)
|
57
|
+
end
|
58
|
+
|
59
|
+
def check_forgery opt
|
60
|
+
workspace = nil
|
61
|
+
result = Crystal::HTTPAdapter.mock_call({}, :check_forgery => opt.to_openobject) do |&block|
|
62
|
+
block.call
|
63
|
+
workspace = crystal[:workspace]
|
64
|
+
end
|
65
|
+
|
66
|
+
workspace.before_request_done.should be_true
|
67
|
+
workspace.after_request_done.should be_true
|
68
|
+
workspace
|
69
|
+
end
|
70
|
+
|
71
|
+
it "should set :authenticity_token only for :get and :html request" do
|
72
|
+
check_forgery(
|
73
|
+
:before_request => lambda{|workspace|
|
74
|
+
workspace.env['REQUEST_METHOD'] = 'GET'
|
75
|
+
workspace.env['CONTENT_TYPE'] = 'html'
|
76
|
+
workspace.class = AnRemote
|
77
|
+
workspace.method_name = 'dumb_method'
|
78
|
+
},
|
79
|
+
:after_request => lambda{|workspace|
|
80
|
+
workspace.request.session['authenticity_token'].should_not be_blank
|
81
|
+
}
|
82
|
+
)
|
83
|
+
|
84
|
+
# post
|
85
|
+
check_forgery(
|
86
|
+
:before_request => lambda{|workspace|
|
87
|
+
workspace.env['REQUEST_METHOD'] = 'POST'
|
88
|
+
workspace.env['CONTENT_TYPE'] = 'html'
|
89
|
+
workspace.class = AnRemote
|
90
|
+
workspace.method_name = 'dumb_method'
|
91
|
+
},
|
92
|
+
:after_request => lambda{|workspace|
|
93
|
+
workspace.request.session['authenticity_token'].should be_blank
|
94
|
+
}
|
95
|
+
)
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should check any non :get request with browser's formats for :authenticity_token" do
|
99
|
+
lambda{
|
100
|
+
check_forgery(
|
101
|
+
:before_request => lambda{|workspace|
|
102
|
+
workspace.env['REQUEST_METHOD'] = 'POST'
|
103
|
+
workspace.env['CONTENT_TYPE'] = 'html'
|
104
|
+
workspace.class = AnRemote
|
105
|
+
workspace.method_name = 'protected_method'
|
106
|
+
}
|
107
|
+
)
|
108
|
+
}.should raise_error(/Invalid authenticity token/)
|
109
|
+
|
110
|
+
# with correct :authenticity_token
|
111
|
+
check_forgery(
|
112
|
+
:before_request => lambda{|workspace|
|
113
|
+
workspace.env['REQUEST_METHOD'] = 'POST'
|
114
|
+
workspace.env['CONTENT_TYPE'] = 'html'
|
115
|
+
workspace.request.session['authenticity_token'] = 'secure token'
|
116
|
+
workspace.params['authenticity_token'] = 'secure token'
|
117
|
+
workspace.class = AnRemote
|
118
|
+
workspace.method_name = 'protected_method'
|
119
|
+
},
|
120
|
+
:after_request => lambda{|workspace|
|
121
|
+
workspace.content.should == "protected result"
|
122
|
+
}
|
123
|
+
)
|
124
|
+
|
125
|
+
# non-browser format
|
126
|
+
check_forgery(
|
127
|
+
:before_request => lambda{|workspace|
|
128
|
+
workspace.env['REQUEST_METHOD'] = 'POST'
|
129
|
+
workspace.env['CONTENT_TYPE'] = 'non-browser-format'
|
130
|
+
workspace.class = AnRemote
|
131
|
+
workspace.method_name = 'protected_method'
|
132
|
+
},
|
133
|
+
:after_request => lambda{|workspace|
|
134
|
+
workspace.content.should == "protected result"
|
135
|
+
}
|
136
|
+
)
|
137
|
+
end
|
138
|
+
|
139
|
+
it "should not protect non protected methods" do
|
140
|
+
check_forgery(
|
141
|
+
:before_request => lambda{|workspace|
|
142
|
+
workspace.env['REQUEST_METHOD'] = 'POST'
|
143
|
+
workspace.env['CONTENT_TYPE'] = 'html'
|
144
|
+
workspace.class = AnRemote
|
145
|
+
workspace.method_name = 'method_without_protection'
|
146
|
+
},
|
147
|
+
:after_request => lambda{|workspace|
|
148
|
+
workspace.content.should == "result"
|
149
|
+
}
|
150
|
+
)
|
151
|
+
end
|
152
|
+
|
153
|
+
it "should use :session_authenticity_token from params (for flash support)" do
|
154
|
+
check_forgery(
|
155
|
+
:before_request => lambda{|workspace|
|
156
|
+
workspace.env['REQUEST_METHOD'] = 'POST'
|
157
|
+
workspace.params.format = 'html'
|
158
|
+
workspace.params['session_authenticity_token'] = 'secure token'
|
159
|
+
workspace.params['authenticity_token'] = 'secure token'
|
160
|
+
workspace.class = AnRemote
|
161
|
+
workspace.method_name = 'protected_method'
|
162
|
+
},
|
163
|
+
:after_request => lambda{|workspace|
|
164
|
+
workspace.content.should == "protected result"
|
165
|
+
}
|
166
|
+
)
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|
data/spec/spec.opts
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require "#{File.expand_path(File.dirname(__FILE__))}/controller_helper"
|
2
|
+
require 'crystal_ext/user_error'
|
3
|
+
require 'crystal_ext/controller_helper'
|
4
|
+
|
5
|
+
describe "User Error" do
|
6
|
+
with_environment :test
|
7
|
+
with_controller_ext_spec
|
8
|
+
|
9
|
+
# before :all do
|
10
|
+
# crystal[:config].environment = :test
|
11
|
+
# crystal[:environment] = Crystal::Environment.new
|
12
|
+
# crystal[:environment].load :controller_ext_spec
|
13
|
+
# end
|
14
|
+
|
15
|
+
after :all do
|
16
|
+
# Micon.clear
|
17
|
+
remove_constants %w(UserErrorSpec)
|
18
|
+
end
|
19
|
+
|
20
|
+
it "user error" do
|
21
|
+
class ::UserErrorSpec
|
22
|
+
inherit Crystal::HttpController
|
23
|
+
|
24
|
+
def call
|
25
|
+
raise_user_error "some error"
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
def catch_user_error
|
30
|
+
begin
|
31
|
+
yield
|
32
|
+
rescue UserError => ue
|
33
|
+
render :inline => "Catched #{ue.message}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
around :catch_user_error
|
37
|
+
end
|
38
|
+
|
39
|
+
ccall(UserErrorSpec, :call).content.should == "Catched some error"
|
40
|
+
end
|
41
|
+
end
|
metadata
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: crystal_ext
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 23
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 4
|
10
|
+
version: 0.0.4
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Alexey Petrushin
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-10-11 00:00:00 +04:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: crystal
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 3
|
30
|
+
segments:
|
31
|
+
- 0
|
32
|
+
version: "0"
|
33
|
+
type: :runtime
|
34
|
+
version_requirements: *id001
|
35
|
+
description:
|
36
|
+
email:
|
37
|
+
executables: []
|
38
|
+
|
39
|
+
extensions: []
|
40
|
+
|
41
|
+
extra_rdoc_files: []
|
42
|
+
|
43
|
+
files:
|
44
|
+
- Rakefile
|
45
|
+
- readme.md
|
46
|
+
- lib/crystal_ext/defer_js.rb
|
47
|
+
- lib/crystal_ext/ensure_no_www.rb
|
48
|
+
- lib/crystal_ext/i18n/locales/ru/pluralization.rb
|
49
|
+
- lib/crystal_ext/i18n.rb
|
50
|
+
- lib/crystal_ext/plugin/app.rb
|
51
|
+
- lib/crystal_ext/plugin/web.rb
|
52
|
+
- lib/crystal_ext/plugin.rb
|
53
|
+
- lib/crystal_ext/prepare_model.rb
|
54
|
+
- lib/crystal_ext/profiles/web_ext.rb
|
55
|
+
- lib/crystal_ext/profiles/web_ext_require.rb
|
56
|
+
- lib/crystal_ext/protect_from_forgery.rb
|
57
|
+
- lib/crystal_ext/user_error.rb
|
58
|
+
- spec/controller_helper.rb
|
59
|
+
- spec/defer_js_spec.rb
|
60
|
+
- spec/helper.rb
|
61
|
+
- spec/i18n_spec/locales/en/general.yml
|
62
|
+
- spec/i18n_spec/locales/ru/general.yml
|
63
|
+
- spec/i18n_spec.rb
|
64
|
+
- spec/prepare_model_spec.rb
|
65
|
+
- spec/protect_from_forgery_spec.rb
|
66
|
+
- spec/spec.opts
|
67
|
+
- spec/user_error_spec.rb
|
68
|
+
has_rdoc: true
|
69
|
+
homepage: http://github.com/alexeypetrushin/crystal_ext
|
70
|
+
licenses: []
|
71
|
+
|
72
|
+
post_install_message:
|
73
|
+
rdoc_options: []
|
74
|
+
|
75
|
+
require_paths:
|
76
|
+
- lib
|
77
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
78
|
+
none: false
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
hash: 3
|
83
|
+
segments:
|
84
|
+
- 0
|
85
|
+
version: "0"
|
86
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
87
|
+
none: false
|
88
|
+
requirements:
|
89
|
+
- - ">="
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
hash: 3
|
92
|
+
segments:
|
93
|
+
- 0
|
94
|
+
version: "0"
|
95
|
+
requirements: []
|
96
|
+
|
97
|
+
rubyforge_project:
|
98
|
+
rubygems_version: 1.3.7
|
99
|
+
signing_key:
|
100
|
+
specification_version: 3
|
101
|
+
summary: Extensions for the Crystal Framework
|
102
|
+
test_files: []
|
103
|
+
|