grape-gen 0.0.1

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