grape-gen 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +31 -0
  6. data/Rakefile +2 -0
  7. data/bin/grape-gen +5 -0
  8. data/grape-scaffold.gemspec +24 -0
  9. data/lib/grape/generate/version.rb +5 -0
  10. data/lib/grape/generate.rb +91 -0
  11. data/template/Gemfile.tt +95 -0
  12. data/template/Guardfile +20 -0
  13. data/template/abilities/api_ability.rb +15 -0
  14. data/template/api/api_app.rb +85 -0
  15. data/template/api/mounts/auth.rb.tt +42 -0
  16. data/template/api/mounts/profile.rb.tt +24 -0
  17. data/template/config/application.yml.tt +33 -0
  18. data/template/config/boot.rb.tt +85 -0
  19. data/template/config/boot_faye.rb +22 -0
  20. data/template/config/boot_sidekiq.rb.tt +33 -0
  21. data/template/config/boot_spec.rb.tt +85 -0
  22. data/template/config/database.rb.tt +6 -0
  23. data/template/config/database.yml.tt +21 -0
  24. data/template/config/initializers/carrierwave.rb +3 -0
  25. data/template/config/initializers/core_ext.rb +14 -0
  26. data/template/config/initializers/em-patches/carrierwave.rb +13 -0
  27. data/template/config/initializers/em-patches/faye.rb +4 -0
  28. data/template/config/initializers/em-patches/lazy_evaluated_pool.rb +35 -0
  29. data/template/config/initializers/em-patches/mandrill.rb +45 -0
  30. data/template/config/initializers/em-patches/redis.rb +13 -0
  31. data/template/config/initializers/em-patches/redis_lazy_evaluated_pool.rb +19 -0
  32. data/template/config/initializers/em-patches/sidekiq.rb +46 -0
  33. data/template/config/initializers/em-patches/tire.rb +8 -0
  34. data/template/config/initializers/faye.rb +3 -0
  35. data/template/config/initializers/grape.rb +11 -0
  36. data/template/config/initializers/logging.rb +38 -0
  37. data/template/config/initializers/mandrill.rb +1 -0
  38. data/template/config/initializers/patches/redis_namespace.rb +96 -0
  39. data/template/config/initializers/sidekiq.rb +15 -0
  40. data/template/config/initializers/tire.rb +6 -0
  41. data/template/config/initializers/workflow.rb +31 -0
  42. data/template/config/logging.yml.tt +39 -0
  43. data/template/config/settings.rb +24 -0
  44. data/template/config/sidekiq.yml.tt +22 -0
  45. data/template/config.ru.tt +8 -0
  46. data/template/faye.ru +55 -0
  47. data/template/jobs/pong_time.rb +11 -0
  48. data/template/lib/faye_auth_extension.rb +61 -0
  49. data/template/lib/faye_publisher.rb +63 -0
  50. data/template/lib/mongoid/tire_plugin.rb +17 -0
  51. data/template/lib/warden/token_strategy.rb +18 -0
  52. data/template/mailers/registration_mailer.rb +17 -0
  53. data/template/models/user.rb.tt +75 -0
  54. data/template/public/faye.html +36 -0
  55. data/template/search_indexes/search_index.rb +60 -0
  56. data/template/search_indexes/user_index.rb +20 -0
  57. data/template/spec/api/mounts/auth_spec.rb.tt +37 -0
  58. data/template/spec/factories/user.rb +8 -0
  59. data/template/spec/spec_helper.rb.tt +132 -0
  60. data/template/uploaders/avatar_uploader.rb +23 -0
  61. data/template/views/v1/user/profile.json.jbuilder.tt +4 -0
  62. metadata +147 -0
data/template/faye.ru ADDED
@@ -0,0 +1,55 @@
1
+ require_relative 'config/boot_faye'
2
+
3
+ Faye::WebSocket.load_adapter('thin')
4
+ Faye.logger = Logging.logger[:faye]
5
+
6
+ class ServerClientSecretAuth
7
+ def initialize(secret)
8
+ @secret = secret
9
+ end
10
+ def outgoing(message, callback)
11
+ if message['channel'] =~ %r{^/meta}
12
+ callback.(message)
13
+ return message
14
+ end
15
+
16
+ message['ext'] ||= {}
17
+ # Set the auth token
18
+ message['ext']['faye_server_secret'] = @secret
19
+
20
+ # Carry on and send the message to the server
21
+ callback.(message)
22
+ message
23
+ end
24
+ end
25
+
26
+ faye_adapter_settings = {
27
+ mount: '/', timeout: 180,
28
+ extensions: [Faye::AuthExtension.new(ApplicationSettings.faye.server_secret)]
29
+ }
30
+
31
+ faye_adapter_settings[:engine] = {
32
+ type: Faye::Redis,
33
+ uri: ApplicationSettings.faye.redis.url
34
+ }
35
+
36
+
37
+ bayeux = Faye::RackAdapter.new(faye_adapter_settings)
38
+
39
+ faye_client = bayeux.get_client
40
+ faye_client.add_extension(ServerClientSecretAuth.new(ApplicationSettings.faye.server_secret))
41
+
42
+ EventMachine.schedule do
43
+ redis_connection = EventMachine::Hiredis.connect(ApplicationSettings.faye.redis[:url])
44
+ redis_connection = Redis::Namespace.new(ApplicationSettings.faye.redis[:namespace], redis: redis_connection) if ApplicationSettings.faye.redis[:namespace]
45
+
46
+ pop = Proc.new do
47
+ redis_connection.blpop('faye.messages',1).callback do |list, msg|
48
+ faye_client.publish(*MultiJson.load(msg).values_at('channel','payload')) if msg
49
+ EventMachine.next_tick(pop)
50
+ end
51
+ end
52
+ EventMachine.next_tick(pop)
53
+ end
54
+
55
+ run bayeux
@@ -0,0 +1,11 @@
1
+ module DelayedJobs
2
+ class PongTime
3
+ include Sidekiq::Worker
4
+
5
+ sidekiq_options unique: true
6
+
7
+ def perform
8
+ FayePublisher.publish('/time',Time.now.utc.to_s)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,61 @@
1
+ require_relative 'warden/token_strategy'
2
+
3
+ module Faye
4
+ class AuthExtension
5
+ def initialize(server_secret)
6
+ @server_secret = server_secret
7
+ end
8
+
9
+ def subscribe_authorized?(env, channel)
10
+ case channel
11
+ when
12
+ '/user/registered', '/time'
13
+ return true
14
+ when %r{^/user/([0-9]+)}
15
+ strategy = TokenStrategy.new(env)
16
+ return false unless strategy.valid? and (strategy.authenticate! == :success)
17
+ strategy.user.id == $1.to_i
18
+ else
19
+ false
20
+ end
21
+ end
22
+
23
+ def incoming(message, callback)
24
+ # Let non-subscribe messages through
25
+ if message['ext'] && (message['ext']['faye_server_secret'] == @server_secret)
26
+ message.delete('ext')
27
+ callback.call(message)
28
+ return message
29
+ end
30
+
31
+ unless message['channel'] =~ %r{^/meta}
32
+ if message['ext'].nil? || (message['ext']['faye_server_secret'] != @server_secret)
33
+ message['error'] = 'Unauthorized'
34
+ end
35
+
36
+ callback.call(message)
37
+ return message
38
+ end
39
+
40
+ unless message['channel'] == '/meta/subscribe'
41
+ callback.call(message)
42
+ return message
43
+ end
44
+
45
+ # Get subscribed channel and auth token
46
+ subscription = message['subscription']
47
+
48
+ message['ext'] ||= {}
49
+
50
+ env = {
51
+ 'HTTP_X_AUTHORIZE' => message['ext']['X-Authorize']
52
+ }
53
+
54
+ message['error'] = 'Unauthorized' unless subscribe_authorized?(Hashie::Mash.new(env), subscription)
55
+
56
+ callback.call(message)
57
+
58
+ message
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,63 @@
1
+ require 'connection_pool'
2
+ require 'em-synchrony/connection_pool'
3
+
4
+ module FayePublisher
5
+ FakePublishing = Struct.new(:channel, :payload)
6
+ class << self
7
+ def fake!
8
+ @fake = true
9
+ end
10
+
11
+ def redis=(arg)
12
+ case arg
13
+ when
14
+ ::ConnectionPool,
15
+ EventMachine::Synchrony::ConnectionPool # Or its descendants
16
+ @redis = arg
17
+ else
18
+ @_config = arg
19
+ end
20
+ end
21
+
22
+ def redis
23
+ @redis ||= begin
24
+ config = ApplicationSettings.faye.redis
25
+ ConnectionPool::Wrapper.new(size: config[:size] || 10) do
26
+ connection = Redis.new(url: config[:url])
27
+ connection = Redis::Namespace.new(config[:namespace], redis: connection) if config[:namespace]
28
+ connection
29
+ end
30
+ end
31
+ end
32
+
33
+ def publish(channel, payload)
34
+ if @fake
35
+ fake_publishings.push(FakePublishing.new(channel, payload))
36
+ return true
37
+ end
38
+ redis.rpush(
39
+ 'faye.messages',
40
+ MultiJson.dump(
41
+ channel: channel,
42
+ payload: payload
43
+ )
44
+ )
45
+ end
46
+
47
+ def configured?; @connection || @_config end
48
+
49
+ def publishings
50
+ raise 'FayePublisher is not configured in fake! mode' unless @fake
51
+ fake_publishings
52
+ end
53
+
54
+ def clear_publishings
55
+ @fake_publishings.clear
56
+ end
57
+
58
+ private
59
+ def fake_publishings
60
+ @fake_publishings ||= []
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,17 @@
1
+ module Mongoid
2
+ module TirePlugin
3
+ extend ActiveSupport::Concern
4
+ included do
5
+ after_save :tire_reindex
6
+ after_destroy :tire_remove
7
+ end
8
+
9
+ private
10
+ def tire_reindex
11
+ SearchIndex.index(self)
12
+ end
13
+ def tire_remove
14
+ SearchIndex.remove(self)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ class TokenStrategy < Warden::Strategies::Base
2
+ def store?; false end
3
+ def valid?
4
+ env['HTTP_X_AUTHORIZE']
5
+ end
6
+ def access_token_type
7
+ :public
8
+ end
9
+ def authenticate!
10
+ user = Models::User.find_by(token: env['HTTP_X_AUTHORIZE'])
11
+
12
+ if user.nil?
13
+ fail!(:no_access)
14
+ else
15
+ success!(user)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,17 @@
1
+ class RegistrationMailer < MandrillMailer::TemplateMailer
2
+ default from: 'welcome@yourdomain.com'
3
+
4
+ def registered(user)
5
+ mandrill_mail(
6
+ template: 'registration',
7
+ subject: 'Welcome',
8
+ to: user.email,
9
+ vars: {
10
+ 'USER_NAME' => user.display_name,
11
+ 'CONFIRMATION_LINK' => user.email_approvement_code
12
+ },
13
+ important: true,
14
+ inline_css: true
15
+ )
16
+ end
17
+ end
@@ -0,0 +1,75 @@
1
+ module Models
2
+ class User
3
+ include Mongoid::Document
4
+ include Mongoid::Timestamps
5
+ include Mongoid::Paranoia
6
+ <%- if @es -%>
7
+ include Mongoid::TirePlugin
8
+ <%- end -%>
9
+ include BCrypt
10
+ include Workflow
11
+
12
+ field :display_name, type: String
13
+ field :email_submit_code, type: String
14
+ field :email
15
+ field :password_hash, type: String
16
+ field :role, type: Symbol, default: :guest
17
+ field :banned, type: Boolean
18
+ field :banned_at, type: DateTime
19
+ field :email_approvement_code, type: String
20
+ field :workflow_state, type: String, default: 'registered'
21
+ field :token, type: String
22
+
23
+ validates_presence_of :display_name, :email, :role
24
+ validates_uniqueness_of :email
25
+ <%- if @carrierwave-%>
26
+ mount_uploader :avatar, Uploaders::Avatar
27
+ <%- end -%>
28
+ workflow do
29
+ state :registered do
30
+ event :email_approved, transition_to: :approved
31
+ end
32
+
33
+ state :approved do
34
+ event :ban, transition_to: :banned
35
+ end
36
+
37
+ state :banned do
38
+ event :unban, transition_to: :approved
39
+ end
40
+ end
41
+
42
+ def email_approved
43
+ self.email_approvement_code = nil
44
+ self.generate_token!
45
+ end
46
+
47
+ def password=(value)
48
+ @password = Password.create(value)
49
+ self.password_hash = @password
50
+ end
51
+
52
+ def password
53
+ @password ||= Password.new(self.password_hash)
54
+ end
55
+
56
+ def generate_approvement_code!
57
+ hash_source = '%s-%s-%s'%[self.email, self.display_name, Time.now]
58
+ self.email_approvement_code = BCrypt::Password.create(hash_source, cost: 3).to_s[8..-1]
59
+ end
60
+
61
+ def generate_token!
62
+ hash_source = '%s-%s-%s'%[self.email, self.display_name, Time.now]
63
+ self.token = BCrypt::Password.create(hash_source, cost: 3).to_s[8..-1]
64
+ end
65
+
66
+ class << self
67
+ def register(fields)
68
+ user = new(fields)
69
+ user.generate_approvement_code!
70
+ user.save!
71
+ user
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,36 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title></title>
6
+ <link rel="stylesheet" href="style.css" />
7
+ <!--[if IE]>
8
+ <script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
9
+ <![endif]-->
10
+ <script src="https://code.jquery.com/jquery-2.1.3.min.js"></script>
11
+ <script src="http://localhost:9393/faye/client.js"></script>
12
+
13
+ <script type="text/javascript">
14
+
15
+
16
+ $(function(){
17
+ var $body = $('body'), client = new Faye.Client('http://localhost:9393/faye/');
18
+
19
+ function subscriptionActive(subscription){
20
+ $body.append('<p>Subscription active '+ subscription +'</p>');
21
+ }
22
+
23
+ client.subscribe('/time', function(data){
24
+ $body.prepend('<p>Current time is '+ data +'</p>');
25
+ }).then(subscriptionActive.bind(null, '/time'));
26
+
27
+ client.subscribe('/user/registered', function(data){
28
+ $body.prepend('<p>Please, welcome '+ data.display_name +'</p>');
29
+ }).then(subscriptionActive.bind(null, '/user/registered'));
30
+ });
31
+ </script>
32
+ </head>
33
+ <body>
34
+
35
+ </body>
36
+ </html>
@@ -0,0 +1,60 @@
1
+ class SearchIndex < Tire::Index
2
+ NoIndexForModel = Class.new(StandardError)
3
+
4
+ def initialize(name)
5
+ super("#{RACK_ENV}_#{name}")
6
+ end
7
+
8
+ def type
9
+ @_type ||= model.to_s.underscore.gsub('/','_')
10
+ end
11
+
12
+ def model
13
+ "Models::#{self.class.name.chomp('Index')}".constantize
14
+ end
15
+
16
+ class << self
17
+ def create_indexes!
18
+ indexes.each do |index|
19
+ index.delete
20
+ index.create
21
+ end
22
+ end
23
+
24
+ def index(instance)
25
+ index = index_by_klass(instance.class)
26
+ index.store(instance)
27
+ rescue NoIndexForModel
28
+ # ignored
29
+ end
30
+
31
+ def remove(instance)
32
+ index = index_by_klass(instance.class)
33
+ index.remove(instance)
34
+ end
35
+
36
+ def reindex!
37
+ indexes.each do |index_instance|
38
+ index_instance.delete
39
+ index_instance.create
40
+ index_instance.model.all.each do |model_instance|
41
+ index(model_instance)
42
+ end
43
+ end
44
+ end
45
+
46
+ private
47
+ def index_by_klass(model_klass)
48
+ @_cache ||= {}
49
+ @_cache[model_klass] and (return @_cache[model_klass])
50
+ idx = indexes.find{|idx| idx.model == model_klass}
51
+ raise NoIndexForModel unless idx
52
+ @_cache[model_klass] = idx
53
+ idx
54
+ end
55
+
56
+ def indexes
57
+ @_indexes ||= ObjectSpace.each_object(Class).select{ |klass| klass < self }.map(&:new)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,20 @@
1
+ class UserIndex < SearchIndex
2
+ def initialize
3
+ super('users')
4
+ end
5
+
6
+ def create
7
+ super(
8
+ mappings: {
9
+ type => {
10
+ # Your mappings here
11
+ }
12
+ }
13
+ )
14
+ end
15
+
16
+ def store(instance)
17
+ index_data = instance.attributes
18
+ super(index_data)
19
+ end
20
+ end
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+ require 'rspec_api_documentation/dsl'
3
+
4
+ resource 'Authorization', focus: true do
5
+ <% if @mandrill %>
6
+ before(:each) { MandrillMailer.deliveries.clear }
7
+ <% end %>
8
+ post '/api/auth/register' do
9
+ example 'New user registration' do
10
+ new_user = FactoryGirl.attributes_for(:user).pick(:display_name, :email)
11
+
12
+ do_request(new_user)
13
+ expect(client.response).to succeed
14
+ <% if @mandrill %>
15
+ email = MandrillMailer::deliveries.detect { |mail|
16
+ mail.template_name == 'registration' &&
17
+ mail.message['to'].any? { |to| to['email'] == new_user[:email] }
18
+ }
19
+ <% end %>
20
+ expect(email).to_not be_nil
21
+ end
22
+ end
23
+
24
+ post '/api/auth/approve_email' do
25
+ example 'User email activation' do
26
+ user = FactoryGirl.create(:user)
27
+ user.generate_approvement_code!
28
+ user.save!
29
+ do_request(email: user.email, email_approvement_code: user.email_approvement_code)
30
+ expect(client.response).to succeed
31
+ expect(response_body).to include('display_name', 'email')
32
+ <% if @faye %>
33
+ expect(FayePublisher.publishings.length).to eq(1)
34
+ <% end %>
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,8 @@
1
+ FactoryGirl.define do
2
+ to_create { |i| i.save }
3
+ factory :user, class: Models::User do
4
+ display_name { Faker::Name.title }
5
+ email { Faker::Internet.email(display_name) }
6
+ role { :guest }
7
+ end
8
+ end
@@ -0,0 +1,132 @@
1
+ require 'rubygems'
2
+ require 'spork'
3
+ require 'rack/test'
4
+ require 'rack/test/methods'
5
+ require 'rspec'
6
+ require 'rspec_api_documentation'
7
+ <% if @mandrill %>
8
+ require 'mandrill_mailer/offline'
9
+ <% end %>
10
+ RACK_ENV = 'test' unless defined?(RACK_ENV)
11
+
12
+ class TestApiClient < RspecApiDocumentation::RackTestClient
13
+ def response_body
14
+ body = JSON.load(last_response.body)
15
+ case body
16
+ when Array
17
+ body.map!{|item| Hashie::Mash.new(item) }
18
+ else
19
+ body
20
+ end
21
+ end
22
+
23
+ def response; last_response end
24
+ end
25
+
26
+ module RspecApiDocumentation::DSL
27
+ module Resource
28
+ def client
29
+ @client ||= TestApiClient.new(self)
30
+ end
31
+ end
32
+ end
33
+
34
+ Spork.prefork do
35
+ require File.expand_path(File.dirname(__FILE__) + '/../config/boot_spec')
36
+ require 'database_cleaner'
37
+ <% if @sidekiq %>
38
+ Sidekiq::Testing.fake!
39
+ <% end %>
40
+ <% if @faye %>
41
+ FayePublisher.fake!
42
+ <% end %>
43
+
44
+ <% if @dev_reload %>
45
+ Grape::RackBuilder.setup do
46
+ force_reloading true
47
+ end
48
+ <% end %>
49
+
50
+ RspecApiDocumentation.configure do |config|
51
+ config.app = RACK_APPLICATION
52
+ end
53
+
54
+ RSpec.configure do |conf|
55
+ FactoryGirl.reload
56
+
57
+ DatabaseCleaner.orm = :mongoid
58
+ DatabaseCleaner.strategy = :truncation
59
+
60
+ conf.include Rack::Test::Methods
61
+
62
+ conf.around(:each) do |example|
63
+ Logging.mdc.clear
64
+ SearchIndex.create_indexes!
65
+ DatabaseCleaner.cleaning { example.run }
66
+ Logging.mdc.clear
67
+ end
68
+ end
69
+
70
+ def app(app = nil, &blk)
71
+ @app ||= block_given? ? app.instance_eval(&blk) : app
72
+ @app ||= RACK_APPLICATION
73
+ end
74
+
75
+ RSpec::Matchers.define :succeed do |valid_codes = nil|
76
+ match do |*args|
77
+ case valid_codes
78
+ when Integer
79
+ actual.status == valid_codes
80
+ when Array
81
+ valid_codes.include?(actual.status)
82
+ else
83
+ (actual.status == 200) || (actual.status == 201)
84
+ end
85
+ end
86
+
87
+ match_when_negated do |*args|
88
+ case valid_codes
89
+ when Integer
90
+ actual.status == valid_codes
91
+ when Array
92
+ valid_codes.include?(actual.status)
93
+ else
94
+ actual.status > 201
95
+ end
96
+ end
97
+
98
+ failure_message do |actual|
99
+ case valid_codes
100
+ when Integer
101
+ "expected that #{actual} succeed with code #{valid_codes}, but got #{actual.status} error:\n#{actual.body}"
102
+ when Array
103
+ "expected that #{actual} succeed with one of #{valid_codes}, but got #{actual.status} error:\n#{actual.body}"
104
+ else
105
+ "expected that #{actual} succeed, but got #{actual.status} error:\n#{actual.body}"
106
+ end
107
+ end
108
+
109
+ failure_message_when_negated do |actual|
110
+ case valid_codes
111
+ when Integer
112
+ "expected that #{actual} fails with code #{valid_codes}, but got #{actual.status} error:\n#{actual.body}"
113
+ when Array
114
+ "expected that #{actual} fails with one of #{valid_codes}, but got #{actual.status} error:\n#{actual.body}"
115
+ else
116
+ "expected that #{actual} fails, but got #{actual.status} error:\n#{actual.body}"
117
+ end
118
+ end
119
+
120
+ description do
121
+ 'respond with 200 or 201 status code'
122
+ end
123
+ end
124
+ end
125
+
126
+ class Spork::Forker
127
+ alias_method :_initialize, :initialize
128
+ def initialize(*args, &block)
129
+ Grape::Reload::Watcher.reload!
130
+ _initialize(*args, &block)
131
+ end
132
+ end
@@ -0,0 +1,23 @@
1
+ module Uploaders
2
+ class Avatar < CarrierWave::Uploader::Base
3
+ include CarrierWave::MiniMagick
4
+ # include CarrierWave::MiniMagick
5
+
6
+ # Choose what kind of storage to use for this uploader:
7
+ storage :file
8
+ # storage :fog
9
+
10
+ # Override the directory where uploaded files will be stored.
11
+ # This is a sensible default for uploaders that are meant to be mounted:
12
+ def store_dir
13
+ "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
14
+ end
15
+
16
+ # Provide a default URL as a default if there hasn't been a file uploaded:
17
+
18
+ # Create different versions of your uploaded files:
19
+ version :thumb do
20
+ process :resize_to_fit => [50, 50]
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,4 @@
1
+ json.(@user, :display_name, :email)
2
+ <% if @carrierwave %>
3
+ json.set! :avatar, @user.avatar.url if @user.avatar?
4
+ <% end %>