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.
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