smooth-io 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. data/.gitignore +2 -0
  2. data/.rvmrc +1 -0
  3. data/.travis +6 -0
  4. data/CNAME +1 -0
  5. data/Gemfile +13 -0
  6. data/Gemfile.lock +147 -0
  7. data/Guardfile +5 -0
  8. data/LICENSE.md +22 -0
  9. data/README.md +33 -0
  10. data/Rakefile +10 -0
  11. data/bin/smooth +8 -0
  12. data/doc/presenters.md +47 -0
  13. data/lib/smooth.rb +56 -0
  14. data/lib/smooth/adapters/rails_cache.rb +19 -0
  15. data/lib/smooth/adapters/redis_cache.rb +36 -0
  16. data/lib/smooth/backends/active_record.rb +56 -0
  17. data/lib/smooth/backends/base.rb +84 -0
  18. data/lib/smooth/backends/file.rb +80 -0
  19. data/lib/smooth/backends/redis.rb +71 -0
  20. data/lib/smooth/backends/redis_namespace.rb +10 -0
  21. data/lib/smooth/backends/rest_client.rb +29 -0
  22. data/lib/smooth/collection.rb +236 -0
  23. data/lib/smooth/collection/cacheable.rb +12 -0
  24. data/lib/smooth/collection/query.rb +14 -0
  25. data/lib/smooth/endpoint.rb +23 -0
  26. data/lib/smooth/meta_data.rb +33 -0
  27. data/lib/smooth/meta_data/application.rb +29 -0
  28. data/lib/smooth/meta_data/inspector.rb +67 -0
  29. data/lib/smooth/model.rb +93 -0
  30. data/lib/smooth/presentable.rb +76 -0
  31. data/lib/smooth/presentable/api_endpoint.rb +49 -0
  32. data/lib/smooth/presentable/chain.rb +46 -0
  33. data/lib/smooth/presentable/controller.rb +39 -0
  34. data/lib/smooth/queryable.rb +35 -0
  35. data/lib/smooth/queryable/converter.rb +8 -0
  36. data/lib/smooth/queryable/settings.rb +23 -0
  37. data/lib/smooth/resource.rb +10 -0
  38. data/lib/smooth/version.rb +3 -0
  39. data/smooth-io.gemspec +35 -0
  40. data/spec/blueprints/people.rb +4 -0
  41. data/spec/environment.rb +17 -0
  42. data/spec/lib/backends/active_record_spec.rb +17 -0
  43. data/spec/lib/backends/base_spec.rb +77 -0
  44. data/spec/lib/backends/file_spec.rb +46 -0
  45. data/spec/lib/backends/multi_spec.rb +47 -0
  46. data/spec/lib/backends/redis_namespace_spec.rb +41 -0
  47. data/spec/lib/backends/redis_spec.rb +40 -0
  48. data/spec/lib/backends/rest_client_spec.rb +0 -0
  49. data/spec/lib/collection/query_spec.rb +9 -0
  50. data/spec/lib/collection_spec.rb +87 -0
  51. data/spec/lib/examples/cacheable_collection_spec.rb +27 -0
  52. data/spec/lib/examples/smooth_model_spec.rb +13 -0
  53. data/spec/lib/meta_data/application_spec.rb +43 -0
  54. data/spec/lib/meta_data_spec.rb +32 -0
  55. data/spec/lib/model_spec.rb +54 -0
  56. data/spec/lib/presentable/api_endpoint_spec.rb +54 -0
  57. data/spec/lib/presentable_spec.rb +80 -0
  58. data/spec/lib/queryable/converter_spec.rb +104 -0
  59. data/spec/lib/queryable_spec.rb +27 -0
  60. data/spec/lib/resource_spec.rb +17 -0
  61. data/spec/lib/smooth_spec.rb +13 -0
  62. data/spec/spec_helper.rb +23 -0
  63. data/spec/support/models.rb +28 -0
  64. data/spec/support/schema.rb +16 -0
  65. metadata +359 -0
@@ -0,0 +1,12 @@
1
+ module Smooth
2
+ class Collection
3
+ module Cacheable
4
+ def create_cache_adapter(options)
5
+ case options[:backend]
6
+ when "redis"
7
+ Smooth::Adapters::RedisCache.new(options)
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -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
@@ -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