smooth-io 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/.rvmrc +1 -0
- data/.travis +6 -0
- data/CNAME +1 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +147 -0
- data/Guardfile +5 -0
- data/LICENSE.md +22 -0
- data/README.md +33 -0
- data/Rakefile +10 -0
- data/bin/smooth +8 -0
- data/doc/presenters.md +47 -0
- data/lib/smooth.rb +56 -0
- data/lib/smooth/adapters/rails_cache.rb +19 -0
- data/lib/smooth/adapters/redis_cache.rb +36 -0
- data/lib/smooth/backends/active_record.rb +56 -0
- data/lib/smooth/backends/base.rb +84 -0
- data/lib/smooth/backends/file.rb +80 -0
- data/lib/smooth/backends/redis.rb +71 -0
- data/lib/smooth/backends/redis_namespace.rb +10 -0
- data/lib/smooth/backends/rest_client.rb +29 -0
- data/lib/smooth/collection.rb +236 -0
- data/lib/smooth/collection/cacheable.rb +12 -0
- data/lib/smooth/collection/query.rb +14 -0
- data/lib/smooth/endpoint.rb +23 -0
- data/lib/smooth/meta_data.rb +33 -0
- data/lib/smooth/meta_data/application.rb +29 -0
- data/lib/smooth/meta_data/inspector.rb +67 -0
- data/lib/smooth/model.rb +93 -0
- data/lib/smooth/presentable.rb +76 -0
- data/lib/smooth/presentable/api_endpoint.rb +49 -0
- data/lib/smooth/presentable/chain.rb +46 -0
- data/lib/smooth/presentable/controller.rb +39 -0
- data/lib/smooth/queryable.rb +35 -0
- data/lib/smooth/queryable/converter.rb +8 -0
- data/lib/smooth/queryable/settings.rb +23 -0
- data/lib/smooth/resource.rb +10 -0
- data/lib/smooth/version.rb +3 -0
- data/smooth-io.gemspec +35 -0
- data/spec/blueprints/people.rb +4 -0
- data/spec/environment.rb +17 -0
- data/spec/lib/backends/active_record_spec.rb +17 -0
- data/spec/lib/backends/base_spec.rb +77 -0
- data/spec/lib/backends/file_spec.rb +46 -0
- data/spec/lib/backends/multi_spec.rb +47 -0
- data/spec/lib/backends/redis_namespace_spec.rb +41 -0
- data/spec/lib/backends/redis_spec.rb +40 -0
- data/spec/lib/backends/rest_client_spec.rb +0 -0
- data/spec/lib/collection/query_spec.rb +9 -0
- data/spec/lib/collection_spec.rb +87 -0
- data/spec/lib/examples/cacheable_collection_spec.rb +27 -0
- data/spec/lib/examples/smooth_model_spec.rb +13 -0
- data/spec/lib/meta_data/application_spec.rb +43 -0
- data/spec/lib/meta_data_spec.rb +32 -0
- data/spec/lib/model_spec.rb +54 -0
- data/spec/lib/presentable/api_endpoint_spec.rb +54 -0
- data/spec/lib/presentable_spec.rb +80 -0
- data/spec/lib/queryable/converter_spec.rb +104 -0
- data/spec/lib/queryable_spec.rb +27 -0
- data/spec/lib/resource_spec.rb +17 -0
- data/spec/lib/smooth_spec.rb +13 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/support/models.rb +28 -0
- data/spec/support/schema.rb +16 -0
- metadata +359 -0
@@ -0,0 +1,14 @@
|
|
1
|
+
module Smooth
|
2
|
+
class Collection
|
3
|
+
class Query < Hash
|
4
|
+
|
5
|
+
def initialize options={}
|
6
|
+
self.merge!(options)
|
7
|
+
end
|
8
|
+
|
9
|
+
def cache_key
|
10
|
+
self.keys.sort.inject([]) {|memo, key| memo << "#{ key }:#{ self.send(:[], key) }" }.join("/")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require "sinatra/base"
|
2
|
+
require "sinatra/json"
|
3
|
+
|
4
|
+
module Smooth
|
5
|
+
class Endpoint < Sinatra::Base
|
6
|
+
helpers Sinatra::JSON
|
7
|
+
|
8
|
+
def self.interface_for(model_class, options={})
|
9
|
+
collection = model_class.collection
|
10
|
+
resource_name = options.fetch(:resource_name, collection.namespace)
|
11
|
+
prefix = options.fetch(:prefix, "/smooth/api/v1")
|
12
|
+
|
13
|
+
get "#{ prefix }/#{ resource_name }" do
|
14
|
+
json collection.query(params)
|
15
|
+
end
|
16
|
+
|
17
|
+
get "#{ prefix }/#{ resource_name }/:id" do
|
18
|
+
json collection.show(params[:id])
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require "smooth/meta_data/inspector"
|
2
|
+
|
3
|
+
module Smooth
|
4
|
+
module MetaData
|
5
|
+
def self.resource_settings
|
6
|
+
@resource_settings ||= {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.register_resource(resource, options={})
|
10
|
+
resource = resource.to_s.camelize.constantize if resource.is_a?(String) or resource.is_a?(Symbol)
|
11
|
+
resource_settings[resource.to_s] = Inspector.new(resource)
|
12
|
+
|
13
|
+
resource.to_s
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.resources
|
17
|
+
resource_settings.inject({}) do |memo,pair|
|
18
|
+
resource_name, inspector = pair
|
19
|
+
memo[resource_name] = inspector.as_json
|
20
|
+
memo
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.available_resources
|
25
|
+
resource_settings.keys
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.[] resource_name
|
29
|
+
resource_settings[resource_name]
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'sinatra'
|
2
|
+
|
3
|
+
module Smooth
|
4
|
+
module MetaData
|
5
|
+
class Application < Sinatra::Base
|
6
|
+
def resources
|
7
|
+
@resources ||= Smooth::MetaData.resources
|
8
|
+
end
|
9
|
+
|
10
|
+
get "/" do
|
11
|
+
content_type :json
|
12
|
+
resources.to_json
|
13
|
+
end
|
14
|
+
|
15
|
+
get "/:resource" do
|
16
|
+
|
17
|
+
content_type :json
|
18
|
+
camelized = params[:resource].camelize
|
19
|
+
info = resources[camelized]
|
20
|
+
|
21
|
+
if !info
|
22
|
+
halt 404 and return
|
23
|
+
end
|
24
|
+
|
25
|
+
info.to_json
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Smooth
|
2
|
+
module MetaData
|
3
|
+
class Inspector
|
4
|
+
attr_accessor :resource
|
5
|
+
|
6
|
+
def initialize(resource)
|
7
|
+
@resource = resource
|
8
|
+
@resource = resource.to_s.camelize.constantize if resource.is_a?(String) or resource.is_a?(Symbol)
|
9
|
+
end
|
10
|
+
|
11
|
+
def presenters
|
12
|
+
methods = [:default]
|
13
|
+
methods += resource.presenter_class.public_methods - Object.methods
|
14
|
+
|
15
|
+
methods.uniq
|
16
|
+
end
|
17
|
+
|
18
|
+
def queryable_parameters
|
19
|
+
resource.queryable_keys
|
20
|
+
end
|
21
|
+
|
22
|
+
def queryable_settings
|
23
|
+
end
|
24
|
+
|
25
|
+
def resource_is_presentable?
|
26
|
+
resource && resource.ancestors.include?(Smooth::Presentable)
|
27
|
+
end
|
28
|
+
|
29
|
+
def resource_is_queryable?
|
30
|
+
resource && resource.ancestors.include?(Smooth::Queryable)
|
31
|
+
end
|
32
|
+
|
33
|
+
def presentable_settings
|
34
|
+
return {} unless resource_is_presentable?
|
35
|
+
|
36
|
+
{
|
37
|
+
presentable: {
|
38
|
+
formats: presenters
|
39
|
+
}
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
def queryable_settings
|
44
|
+
return {} unless resource_is_queryable?
|
45
|
+
|
46
|
+
{
|
47
|
+
queryable:{
|
48
|
+
parameters: queryable_parameters,
|
49
|
+
settings: resource.smooth_queryable_settings.to_hash
|
50
|
+
}
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_hash
|
55
|
+
hash = {}
|
56
|
+
hash.merge!(presentable_settings)
|
57
|
+
hash.merge!(queryable_settings)
|
58
|
+
|
59
|
+
hash
|
60
|
+
end
|
61
|
+
|
62
|
+
def as_json
|
63
|
+
to_hash
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
data/lib/smooth/model.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Smooth
|
4
|
+
class Model
|
5
|
+
include Virtus
|
6
|
+
|
7
|
+
attr_accessor :model_attributes, :collection, :model_options
|
8
|
+
|
9
|
+
InvalidRecord = Class.new(Exception)
|
10
|
+
InvalidCollection = Class.new(Exception)
|
11
|
+
|
12
|
+
def initialize(attributes={},options={})
|
13
|
+
options ||= {}
|
14
|
+
attributes ||= {}
|
15
|
+
|
16
|
+
@model_options = options.dup
|
17
|
+
@collection = options[:collection] if options[:collection]
|
18
|
+
|
19
|
+
raise InvalidRecord unless attributes.is_a?(Hash)
|
20
|
+
|
21
|
+
unless respond_to?(:id)
|
22
|
+
extend(Virtus)
|
23
|
+
self.class.attribute :id, String unless respond_to?(:id)
|
24
|
+
end
|
25
|
+
|
26
|
+
super(attributes)
|
27
|
+
end
|
28
|
+
|
29
|
+
# This should delegate to the collection sync method
|
30
|
+
# which is capable of getting a single record
|
31
|
+
def sync method=:read, *args
|
32
|
+
raise InvalidCollection unless collection && collection.respond_to?(:sync)
|
33
|
+
|
34
|
+
case
|
35
|
+
|
36
|
+
when is_new?
|
37
|
+
self.id = collection.sync(:create, self).id
|
38
|
+
when !is_new? && method == :update
|
39
|
+
collection.sync(:update, self)
|
40
|
+
else
|
41
|
+
collection.sync(:read)
|
42
|
+
end
|
43
|
+
|
44
|
+
fetch
|
45
|
+
end
|
46
|
+
|
47
|
+
def save
|
48
|
+
is_new? ? sync(:create) : sync(:update)
|
49
|
+
end
|
50
|
+
|
51
|
+
def update_attributes attributes={}
|
52
|
+
self.send(:set_attributes, attributes)
|
53
|
+
save
|
54
|
+
end
|
55
|
+
|
56
|
+
# the collection should implement this single object find
|
57
|
+
def fetch
|
58
|
+
return self unless self.id
|
59
|
+
|
60
|
+
model = collection.models.detect do |item|
|
61
|
+
item[id_field].to_s == self.id.to_s
|
62
|
+
end
|
63
|
+
|
64
|
+
if model && model.attributes.is_a?(Hash)
|
65
|
+
self.send(:set_attributes, model.attributes)
|
66
|
+
end
|
67
|
+
|
68
|
+
self
|
69
|
+
end
|
70
|
+
|
71
|
+
def is_new?
|
72
|
+
id.nil?
|
73
|
+
end
|
74
|
+
|
75
|
+
def id
|
76
|
+
attributes.fetch(:id, nil)
|
77
|
+
end
|
78
|
+
|
79
|
+
def as_json options={}
|
80
|
+
to_hash rescue @attributes
|
81
|
+
end
|
82
|
+
|
83
|
+
def to_json options={}
|
84
|
+
JSON.generate(as_json(options))
|
85
|
+
end
|
86
|
+
|
87
|
+
protected
|
88
|
+
|
89
|
+
def id_field
|
90
|
+
model_options.fetch(:id_field, :id)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module Smooth
|
2
|
+
|
3
|
+
class DefaultPresenter
|
4
|
+
def default
|
5
|
+
[:id]
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
module Presentable
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
|
12
|
+
included do
|
13
|
+
Smooth::MetaData.register_resource(self)
|
14
|
+
end
|
15
|
+
|
16
|
+
def present_as(format=:default)
|
17
|
+
record = self
|
18
|
+
keys = self.class.presenter_class && self.class.presenter_class.respond_to?(format) && self.class.presenter_class.send(format)
|
19
|
+
keys ||= self.class.default_presenter_attributes
|
20
|
+
|
21
|
+
Array(keys).inject({}) do |memo,item|
|
22
|
+
AttributeDelegator.pluck(record, item, memo)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class AttributeDelegator
|
27
|
+
def self.pluck(record, attribute, memo)
|
28
|
+
if attribute.is_a?(Symbol) || attribute.is_a?(String)
|
29
|
+
memo[attribute] = record.send(attribute)
|
30
|
+
return memo
|
31
|
+
end
|
32
|
+
|
33
|
+
if attribute.is_a?(Hash)
|
34
|
+
key = attribute[:attribute] || attribute[:key]
|
35
|
+
meth = attribute[:method] || key
|
36
|
+
presenter = attribute[:presenter] || :default
|
37
|
+
|
38
|
+
value = record.send(meth)
|
39
|
+
|
40
|
+
memo[key] = value.send(:present_as, presenter)
|
41
|
+
end
|
42
|
+
|
43
|
+
return memo
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
module ClassMethods
|
48
|
+
def presenter_class
|
49
|
+
"#{ to_s }Presenter".constantize rescue nil
|
50
|
+
end
|
51
|
+
|
52
|
+
def default_presenter_attributes
|
53
|
+
if respond_to?(:column_names)
|
54
|
+
column_names.map(&:to_sym)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def presenter_format_for_role(recipient=:default, format=:default)
|
59
|
+
if presenter_class.respond_to?("#{ recipient }_#{ format }")
|
60
|
+
"#{ recipient }_#{ format }"
|
61
|
+
elsif presenter_class.respond_to?("#{ format }")
|
62
|
+
format
|
63
|
+
else
|
64
|
+
:default
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def present params={}
|
69
|
+
scope = scoped
|
70
|
+
scope = query(params) if ancestors.include?(Smooth::Queryable)
|
71
|
+
|
72
|
+
Smooth::Presentable::Chain.new(scope)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Smooth
|
2
|
+
module Presentable
|
3
|
+
class ApiEndpoint < Sinatra::Base
|
4
|
+
|
5
|
+
get "/:resource" do
|
6
|
+
halt 404 unless valid_resource?
|
7
|
+
index.to_json
|
8
|
+
end
|
9
|
+
|
10
|
+
get "/:resource/:presenter" do
|
11
|
+
halt 404 unless valid_resource?
|
12
|
+
index.to_json
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
def resource_model
|
17
|
+
params[:resource] && params[:resource].singularize.camelize.constantize rescue nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def valid_resource?
|
21
|
+
resource_model && resource_model.ancestors.include?(Smooth::Presentable)
|
22
|
+
end
|
23
|
+
|
24
|
+
def presenter_format
|
25
|
+
params[:presenter] || params[:presenter_format] || :default
|
26
|
+
end
|
27
|
+
|
28
|
+
def base_scope
|
29
|
+
resource_model
|
30
|
+
end
|
31
|
+
|
32
|
+
def current_user_role
|
33
|
+
:default
|
34
|
+
end
|
35
|
+
|
36
|
+
def index
|
37
|
+
base_scope.present(params)
|
38
|
+
.as(presenter_format)
|
39
|
+
.to(current_user_role).results
|
40
|
+
end
|
41
|
+
|
42
|
+
def show
|
43
|
+
record = base_scope.find(params[:id])
|
44
|
+
record.present_as(presenter_format)
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Smooth
|
2
|
+
module Presentable
|
3
|
+
# The Chain allows for constructing an API call with the following:
|
4
|
+
#
|
5
|
+
# Resource.present(query)
|
6
|
+
# .as(format)
|
7
|
+
# .to(user)
|
8
|
+
#
|
9
|
+
class Chain
|
10
|
+
attr_accessor :scope, :format, :recipient, :presenter_method
|
11
|
+
|
12
|
+
def initialize(scope)
|
13
|
+
@scope = scope
|
14
|
+
@format = :default
|
15
|
+
@recipient = :default
|
16
|
+
end
|
17
|
+
|
18
|
+
def as(format=:default)
|
19
|
+
@format = format
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
def to(recipient=:default)
|
24
|
+
@recipient = recipient
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def results
|
29
|
+
@presenter_method ||= scope.klass.presenter_format_for_role(recipient, format)
|
30
|
+
|
31
|
+
@results ||= scope.map do |record|
|
32
|
+
record.present_as(presenter_method)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_a
|
37
|
+
results
|
38
|
+
end
|
39
|
+
|
40
|
+
def method_missing meth, *args, &block
|
41
|
+
results.send(meth, *args, &block)
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|