lightrail 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/lightrail +1 -0
- data/lib/lightrail/action_controller.rb +3 -0
- data/lib/lightrail/action_controller/haltable.rb +26 -0
- data/lib/lightrail/action_controller/metal.rb +42 -0
- data/lib/lightrail/action_controller/param.rb +12 -0
- data/lib/lightrail/core_ext/regexp.rb +7 -0
- data/lib/lightrail/encryptor.rb +62 -0
- data/lib/lightrail/models/errors.rb +21 -0
- data/lib/lightrail/railtie.rb +34 -0
- data/lib/lightrail/version.rb +3 -0
- data/lib/lightrail/wrapper.rb +65 -0
- data/lib/lightrail/wrapper/active_record.rb +15 -0
- data/lib/lightrail/wrapper/associations.rb +49 -0
- data/lib/lightrail/wrapper/controller.rb +55 -0
- data/lib/lightrail/wrapper/model.rb +114 -0
- metadata +78 -0
data/bin/lightrail
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "rails/cli"
|
@@ -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,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,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:
|