lightrail 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1 @@
1
+ require "rails/cli"
@@ -0,0 +1,3 @@
1
+ require 'action_controller'
2
+ require 'active_support/concern'
3
+ require 'lightrail/action_controller/metal'
@@ -0,0 +1,26 @@
1
+ module Lightrail
2
+ module ActionController
3
+ module Haltable
4
+ def process_action(*)
5
+ opts = catch :halt do
6
+ return super
7
+ end
8
+
9
+ if opts
10
+ render opts
11
+ end
12
+ end
13
+
14
+ def halt(opts = nil)
15
+ throw :halt, opts
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ ActionController::Renderers.add :errors do |errors, opts|
22
+ json = { :errors => errors }.to_json(opts)
23
+ self.status = 422 if status == 200
24
+ self.content_type ||= Mime::JSON
25
+ self.response_body = json
26
+ end
@@ -0,0 +1,42 @@
1
+ require 'lightrail/action_controller/haltable'
2
+ require 'lightrail/action_controller/param'
3
+
4
+ module Lightrail
5
+ module ActionController
6
+ # A configured ActionController::Metal that includes
7
+ # only the basic functionality.
8
+ class Metal < ::ActionController::Metal
9
+ abstract!
10
+
11
+ include ::ActionController::UrlFor
12
+ include ::ActionController::Head
13
+ include ::ActionController::Redirecting
14
+ include ::ActionController::Rendering
15
+ include ::ActionController::Renderers::All
16
+ include ::ActionController::ConditionalGet
17
+ include ::ActionController::RackDelegation
18
+ include ::ActionController::Caching
19
+ include ::ActionController::MimeResponds
20
+ include ::ActionController::ImplicitRender
21
+
22
+ include ::ActionController::ForceSSL if defined?(::ActionController::ForceSSL)
23
+ include ::ActionController::HttpAuthentication::Basic::ControllerMethods
24
+ include ::ActionController::HttpAuthentication::Digest::ControllerMethods
25
+ include ::ActionController::HttpAuthentication::Token::ControllerMethods
26
+
27
+ # Before callbacks should also be executed the earliest as possible, so
28
+ # also include them at the bottom.
29
+ include ::AbstractController::Callbacks
30
+
31
+ # The same with rescue, append it at the end to wrap as much as possible.
32
+ include ::ActionController::Rescue
33
+
34
+ # Add instrumentations hooks at the bottom, to ensure they instrument
35
+ # all the methods properly.
36
+ include ::ActionController::Instrumentation
37
+
38
+ include Haltable
39
+ include Param
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,12 @@
1
+ module Lightrail
2
+ module ActionController
3
+ module Param
4
+ def param(key)
5
+ key.to_s.split('.').inject(params) do |param, key|
6
+ return unless param
7
+ param[key]
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,7 @@
1
+ class Regexp
2
+ class << self
3
+ def email
4
+ /^([\w\.%\+\-]+)@([\w\-]+\.)+([\w]{2,})$/i
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,62 @@
1
+ require 'openssl'
2
+
3
+ module Lightrail
4
+ class Encryptor
5
+ class InvalidMessage < StandardError; end
6
+ OpenSSLCipherError = OpenSSL::Cipher.const_defined?(:CipherError) ? OpenSSL::Cipher::CipherError : OpenSSL::CipherError
7
+
8
+ attr_accessor :addon_secret
9
+
10
+ def initialize(secret)
11
+ @addon_secret = secret
12
+ end
13
+
14
+ def encrypt(msg, initvec = nil)
15
+ msg = msg.to_s
16
+ cipher = new_cipher
17
+ initvec = decode64(initvec) if initvec
18
+ initvec ||= cipher.random_iv
19
+
20
+ cipher.encrypt
21
+ cipher.key = decode64(addon_secret)
22
+ cipher.iv = initvec
23
+
24
+ encrypted = cipher.update(msg)
25
+ encrypted << cipher.final
26
+
27
+ [ encrypted, initvec ].
28
+ map { |bytes| encode64(bytes) }.
29
+ join("--")
30
+ end
31
+
32
+ def decrypt(encrypted_message)
33
+ cipher = new_cipher
34
+ encrypted_data, iv = encrypted_message.split("--").map { |v| decode64(v) }
35
+
36
+ cipher.decrypt
37
+ cipher.key = decode64(addon_secret)
38
+ cipher.iv = iv
39
+
40
+ decrypted_data = cipher.update(encrypted_data)
41
+ decrypted_data << cipher.final
42
+
43
+ decrypted_data
44
+ rescue OpenSSLCipherError, TypeError
45
+ raise InvalidMessage
46
+ end
47
+
48
+ private
49
+
50
+ def decode64(s)
51
+ ActiveSupport::Base64.decode64(s)
52
+ end
53
+
54
+ def encode64(s)
55
+ ActiveSupport::Base64.encode64s(s)
56
+ end
57
+
58
+ def new_cipher
59
+ OpenSSL::Cipher::Cipher.new("AES-256-CBC")
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,21 @@
1
+ require 'active_model/errors'
2
+
3
+ module Lightrail
4
+ module Models
5
+ module Errors
6
+ def errors
7
+ @errors ||= Lightrail::Models::Errors::Errors.new(self)
8
+ end
9
+
10
+ class Errors < ::ActiveModel::Errors
11
+ # Only show one message per error key.
12
+ def as_json(options=nil)
13
+ hash = ActiveSupport::OrderedHash.new
14
+ target = @messages || self
15
+ target.each_key { |k| hash[k] = target[k][0] }
16
+ hash
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,34 @@
1
+ require "lightrail/action_controller"
2
+ require "rails/railtie"
3
+
4
+ module Lightrail
5
+ class Railtie < Rails::Railtie
6
+ class Wrapper
7
+ def initialize(config)
8
+ @config = config
9
+ end
10
+
11
+ def remove_session_middlewares!
12
+ @config.app_middleware.delete "ActionDispatch::Cookies"
13
+ @config.app_middleware.delete "ActionDispatch::Session::CookieStore"
14
+ @config.app_middleware.delete "ActionDispatch::Flash"
15
+ end
16
+
17
+ def remove_browser_middlewares!
18
+ @config.app_middleware.delete "ActionDispatch::BestStandardsSupport"
19
+ end
20
+ end
21
+
22
+ config.lightrail = Wrapper.new(config)
23
+
24
+ initializer "lightrail.action_controller.set_config" do |app|
25
+ Lightrail::ActionController::Metal.class_eval do
26
+ include app.routes.url_helpers
27
+ self.logger ||= Rails.logger
28
+ self.cache_store ||= RAILS_CACHE
29
+ prepend_view_path "app/views"
30
+ app.config.action_controller.each { |k,v| send("#{k}=", v) rescue nil }
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,3 @@
1
+ module Lightrail
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,65 @@
1
+ module Lightrail
2
+ module Wrapper
3
+ class Result < Hash
4
+ attr_reader :associations
5
+
6
+ def initialize
7
+ super
8
+ @associations = Hash.new { |h,k| h[k] = [] }
9
+ end
10
+ end
11
+
12
+ class << self
13
+ # Receives an object and returns its wrapper.
14
+ def find(model)
15
+ klass = model.is_a?(Class) ? model : model.class
16
+ ActiveSupport::Dependencies.constantize("#{klass.name}Wrapper")
17
+ end
18
+
19
+ # Receives an object, find its wrapper and render it.
20
+ # If the object is an array, loop for each element in the
21
+ # array and call +render_many+ (instead of +render+).
22
+ def render(object, scope, options={}, result={})
23
+ result = Result.new.merge!(result)
24
+
25
+ if object.blank?
26
+ handle_blank(object, result, options)
27
+ elsif object.respond_to?(:each)
28
+ object.each { |i| Lightrail::Wrapper.find(i).new(i, scope).render_many(options, result) }
29
+ else
30
+ Lightrail::Wrapper.find(object).new(object, scope).render(options, result)
31
+ end
32
+
33
+ result.associations.inject(result) do |final, (key, value)|
34
+ value.uniq!
35
+ render_association(value, scope, { :as => key }, final)
36
+ end
37
+ end
38
+
39
+ def render_association(value, scope, options, result)
40
+ if value.empty?
41
+ handle_blank(value, result, options)
42
+ result
43
+ else
44
+ wrapper = Lightrail::Wrapper.find(value.first)
45
+ wrapper.around_association(value, scope) do
46
+ value.each { |i| wrapper.new(i, scope).render_many(options, result) }
47
+ result
48
+ end
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def handle_blank(object, result, options)
55
+ name = options[:as] || options[:fallback]
56
+ name ||= object ? "resources" : "resource"
57
+ result[name] = object
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ require "lightrail/wrapper/model"
64
+ require "lightrail/wrapper/controller"
65
+ require "lightrail/wrapper/active_record"
@@ -0,0 +1,15 @@
1
+ module Lightrail
2
+ module Wrapper
3
+ module ActiveRecord
4
+ def slice(*attrs)
5
+ attrs.map! { |a| a.to_s }
6
+ attributes.slice(*attrs)
7
+ end
8
+ end
9
+ end
10
+ end
11
+
12
+ ActiveSupport.on_load(:active_record) do
13
+ include Lightrail::Wrapper::ActiveRecord
14
+ end
15
+
@@ -0,0 +1,49 @@
1
+ module Lightrail
2
+ module Wrapper
3
+ module Associations
4
+ # +association+ returns the name of the association in the wrapped object.
5
+ # +key+ returns the JSON key to store the +association+ id.
6
+ # +as+ is the name of the +association+ in the flat JSON.
7
+ # +includes+ is the name expected on the include.
8
+ class AssociationConfig
9
+ attr_reader :name, :as, :key, :includes
10
+
11
+ def initialize(name, options)
12
+ @name = name.to_sym
13
+ end
14
+
15
+ # Receives a view and update it accordingly with the given object id
16
+ # and the association cardinality.
17
+ def update(view, object)
18
+ raise NotImplementedError
19
+ end
20
+ end
21
+
22
+ class HasOneConfig < AssociationConfig
23
+ def initialize(name, options)
24
+ super
25
+ @as = options[:as] || name.to_s.pluralize.to_sym
26
+ @key = options[:key] || :"#{name}_id"
27
+ @includes = options[:includes] || name.to_s.pluralize
28
+ end
29
+
30
+ def update(view, object)
31
+ view[@key] = object && object.id
32
+ end
33
+ end
34
+
35
+ class HasManyConfig < AssociationConfig
36
+ def initialize(name, options)
37
+ super
38
+ @as = options[:as] || name.to_sym
39
+ @key = options[:key] || name.to_sym
40
+ @includes = options[:includes] || name.to_s
41
+ end
42
+
43
+ def update(view, object)
44
+ view[@key] = object.map(&:id)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,55 @@
1
+ module Lightrail
2
+ module Wrapper
3
+ module Controller
4
+ extend ActiveSupport::Concern
5
+
6
+ private
7
+
8
+ # Receives an object and render it as JSON considering its respective wrapper.
9
+ def json(object, options={})
10
+ json = Lightrail::Wrapper.render(object, wrapper_scope,
11
+ :include => wrapper_includes, :fallback => controller_name, :as => options.delete(:as))
12
+ render options.merge(:json => json)
13
+ end
14
+
15
+ def errors(object)
16
+ render :errors => { object.class.model_name.underscore => object.errors }
17
+ end
18
+
19
+ # Receives a relation and add the appropriate includes and configuration.
20
+ # Useful when wrapping up arrays to avoid N+1 queries in the database.
21
+ def wrap_array(array)
22
+ array = array.to_a
23
+ return array if array.empty?
24
+
25
+ klass = array[0].class
26
+ valid = Lightrail::Wrapper.find(klass).valid_includes(wrapper_includes)
27
+ return array if valid.empty?
28
+
29
+ klass.send(:preload_associations, array, valid)
30
+ array
31
+ end
32
+
33
+ # Returns a wrapper around the given object.
34
+ def wrapper(object, scope=wrapper_scope)
35
+ Lightrail::Wrapper.find(object).new(object, scope)
36
+ end
37
+
38
+ # Returns given includes as a nested hash.
39
+ def wrapper_includes
40
+ @wrapper_includes ||= params[:include].to_s.split(",")
41
+ end
42
+
43
+ # Pass the relevant scope to the wrapper object.
44
+ def wrapper_scope
45
+ raise NotImplementedError, "wrapper_scope needs to be implemented according to your application"
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ ActiveSupport.on_load(:action_controller) do
52
+ include Lightrail::Wrapper::Controller
53
+ end
54
+
55
+ Lightrail::ActionController::Metal.send :include, Lightrail::Wrapper::Controller
@@ -0,0 +1,114 @@
1
+ require "lightrail/wrapper/associations"
2
+
3
+ module Lightrail
4
+ module Wrapper
5
+ class Model
6
+ class_attribute :associations
7
+ self.associations = []
8
+
9
+ class << self
10
+ def inherited(base)
11
+ base.class_eval do
12
+ alias_method wrapped_class.model_name.underscore, :resource
13
+ end
14
+ end
15
+
16
+ def around_association(collection, scope)
17
+ yield
18
+ end
19
+
20
+ # Declares that this object is associated to the given association
21
+ # with cardinality 1..1.
22
+ def has_one(*associations)
23
+ options = associations.extract_options!
24
+ self.associations += associations.map { |a| Associations::HasOneConfig.new(a, options) }
25
+ define_association_methods(associations)
26
+ end
27
+
28
+ # Declares that this object is associated to the given association
29
+ # with cardinality 1..*.
30
+ def has_many(*associations)
31
+ options = associations.extract_options!
32
+ self.associations += associations.map { |a| Associations::HasManyConfig.new(a, options) }
33
+ define_association_methods(associations)
34
+ end
35
+
36
+ # Based on the declared associations and the given parameters,
37
+ # generate an includes clause that can be passed down to the relation
38
+ # object to avoid doing N+1 queries.
39
+ def valid_includes(includes)
40
+ result = []
41
+ includes.each do |i|
42
+ if association = associations.find { |a| a.includes == i }
43
+ result << association.name
44
+ end
45
+ end
46
+ result
47
+ end
48
+
49
+ # Returns the original class wrapped by this wrapper.
50
+ def wrapped_class
51
+ @wrapped_class ||= name.sub(/Wrapper$/, "").constantize
52
+ end
53
+
54
+ private
55
+
56
+ def define_association_methods(associations)
57
+ associations.each do |association|
58
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
59
+ def #{association}
60
+ @resource.#{association}
61
+ end
62
+ RUBY
63
+ end
64
+ end
65
+ end
66
+
67
+ attr_reader :resource, :scope
68
+
69
+ def initialize(resource, scope)
70
+ @resource = resource
71
+ @scope = scope
72
+ end
73
+
74
+ def view
75
+ raise NotImplementedError, "No view implemented for #{self}"
76
+ end
77
+
78
+ # Gets the view and render the associated object.
79
+ def render(options={}, result=Result.new)
80
+ name = options[:as] || wrapped_model_name.underscore
81
+ result[name] = view = self.view
82
+ _include_associations(result, view, options[:include])
83
+ end
84
+
85
+ # Gets the view and render the associated considering it is part of a collection.
86
+ def render_many(options={}, result=Result.new)
87
+ name = options[:as] || wrapped_model_name.plural
88
+ view = self.view
89
+ array = (result[name] ||= []) << view
90
+ _include_associations(result, view, options[:include])
91
+ end
92
+
93
+ private
94
+
95
+ def _include_associations(result, view, includes)
96
+ return result if includes.blank?
97
+ associations = result.associations
98
+
99
+ self.class.associations.each do |config|
100
+ next unless includes.include?(config.includes)
101
+ object = send(config.name)
102
+ config.update(view, object)
103
+ associations[config.as].concat Array.wrap(object)
104
+ end
105
+
106
+ result
107
+ end
108
+
109
+ def wrapped_model_name
110
+ self.class.wrapped_class.model_name
111
+ end
112
+ end
113
+ end
114
+ end
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lightrail
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Carl Lerche
9
+ - José Valim
10
+ - Tony Arcieri
11
+ autorequire:
12
+ bindir: bin
13
+ cert_chain: []
14
+ date: 2012-03-08 00:00:00.000000000 Z
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: railties
18
+ requirement: &70316991038220 !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - ~>
22
+ - !ruby/object:Gem::Version
23
+ version: 3.2.0
24
+ type: :runtime
25
+ prerelease: false
26
+ version_requirements: *70316991038220
27
+ description: Lightrail slims Rails down to the bare essentials great JSON web services
28
+ crave
29
+ email:
30
+ - me@carllerche.com
31
+ - jose.valim@gmail.com
32
+ - tony.arcieri@gmail.com
33
+ executables:
34
+ - lightrail
35
+ extensions: []
36
+ extra_rdoc_files: []
37
+ files:
38
+ - lib/lightrail/action_controller/haltable.rb
39
+ - lib/lightrail/action_controller/metal.rb
40
+ - lib/lightrail/action_controller/param.rb
41
+ - lib/lightrail/action_controller.rb
42
+ - lib/lightrail/core_ext/regexp.rb
43
+ - lib/lightrail/encryptor.rb
44
+ - lib/lightrail/models/errors.rb
45
+ - lib/lightrail/railtie.rb
46
+ - lib/lightrail/version.rb
47
+ - lib/lightrail/wrapper/active_record.rb
48
+ - lib/lightrail/wrapper/associations.rb
49
+ - lib/lightrail/wrapper/controller.rb
50
+ - lib/lightrail/wrapper/model.rb
51
+ - lib/lightrail/wrapper.rb
52
+ - bin/lightrail
53
+ homepage: http://github.com/tarcieri/lightrail
54
+ licenses: []
55
+ post_install_message:
56
+ rdoc_options: []
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ none: false
61
+ requirements:
62
+ - - ! '>='
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ! '>='
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ requirements: []
72
+ rubyforge_project:
73
+ rubygems_version: 1.8.10
74
+ signing_key:
75
+ specification_version: 3
76
+ summary: Slim Rails stack for JSON services
77
+ test_files: []
78
+ has_rdoc: