lightrail 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.
@@ -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: