smooth-io 0.0.5
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.
- 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
|