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,84 @@
1
+ module Smooth
2
+ module Backends
3
+ class Base
4
+
5
+ def initialize options={}
6
+ @id_counter = 0
7
+ @maximum_updated_at = Time.now.to_i
8
+ @namespace = options[:namespace]
9
+
10
+ records = options[:records] || {}
11
+
12
+ if records.is_a?(Array)
13
+ records = records.select {|r| r.is_a?(Hash) }.inject({}) do |memo,record|
14
+ record.symbolize_keys!
15
+ record[:id] ||= @id_counter += 1
16
+ memo[ record[:id] ] = record
17
+ memo
18
+ end
19
+ end
20
+
21
+ @storage = {id_counter: @id_counter, records: records, maximum_updated_at: @maximum_updated_at}
22
+ end
23
+
24
+ def records
25
+ @storage[:records]
26
+ end
27
+
28
+ def maximum_updated_at
29
+ @storage[:maximum_updated_at]
30
+ end
31
+
32
+ def index
33
+ records.values
34
+ end
35
+
36
+ def show id
37
+ records[id.to_i]
38
+ end
39
+
40
+ def update attributes={}
41
+ attributes.symbolize_keys!
42
+ @storage[:maximum_updated_at] = attributes[:updated_at] = Time.now.to_i
43
+ record = records[attributes[:id]]
44
+ record.merge!(attributes)
45
+ record
46
+ end
47
+
48
+ def create attributes={}
49
+ attributes.symbolize_keys!
50
+ attributes[:id] = increment_id
51
+ @storage[:maximum_updated_at] = attributes[:created_at] = attributes[:updated_at] = Time.now.to_i
52
+ records[attributes[:id]] ||= attributes
53
+ end
54
+
55
+ def destroy id
56
+ record = records.delete(id)
57
+ !record.nil?
58
+ end
59
+
60
+ # A Naive query method which only matches for equality
61
+ def query params={}
62
+ params.symbolize_keys!
63
+
64
+ index.select do |record|
65
+ record.symbolize_keys!
66
+
67
+ params.keys.all? do |key|
68
+ record[key] == params[key]
69
+ end
70
+ end
71
+ end
72
+
73
+ protected
74
+
75
+ def increment_id
76
+ @id_counter += 1
77
+ end
78
+
79
+ def storage=(object={})
80
+ @storage = object
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,80 @@
1
+ module Smooth
2
+ module Backends
3
+ class File < Base
4
+
5
+ class << self
6
+ attr_accessor :data_directory, :flush_threshold
7
+ end
8
+
9
+ self.data_directory = Smooth.data_directory
10
+ self.flush_threshold = 300
11
+
12
+ FileUtils.mkdir_p(data_directory)
13
+
14
+ attr_accessor :storage,
15
+ :namespace
16
+
17
+ def initialize options={}
18
+ super
19
+
20
+ @data_directory = options[:data_directory]
21
+ restore if ::File.exists?(storage_path)
22
+
23
+ setup_periodic_flushing
24
+ end
25
+
26
+
27
+ def storage_path
28
+ ::File.join(data_directory, "#{namespace}.json")
29
+ end
30
+
31
+ def url
32
+ "file://#{ storage_path }"
33
+ end
34
+
35
+
36
+ def kill
37
+ @periodic_flusher && @periodic_flusher.kill
38
+ end
39
+
40
+ def setup_periodic_flushing
41
+ @periodic_flusher ||= Thread.new do
42
+ while true
43
+ flush
44
+ sleep 20
45
+ end
46
+ end
47
+ end
48
+
49
+ protected
50
+
51
+ def throttled?
52
+ @last_flushed_at && (Time.now.to_i - @last_flushed_at) < self.class.flush_threshold
53
+ end
54
+
55
+ def restore
56
+ from_disk = JSON.parse( IO.read(storage_path) ) rescue {}
57
+
58
+ unless from_disk[:maximum_updated_at] && from_disk[:id_counter] && from_disk[:records]
59
+ return
60
+ end
61
+
62
+ @storage = from_disk && true
63
+ end
64
+
65
+ def flush force=false
66
+ return if !force && throttled?
67
+
68
+ ::File.open(storage_path,'w+') do |fh|
69
+ fh.puts( JSON.generate(storage) )
70
+ @last_flushed_at = Time.now.to_i
71
+ end
72
+ end
73
+
74
+ def data_directory
75
+ @data_directory || self.class.data_directory
76
+ end
77
+
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,71 @@
1
+ module Smooth
2
+ module Backends
3
+ class Redis < Base
4
+ attr_reader :connection,
5
+ :namespace
6
+
7
+ def initialize options={}
8
+ redis = ::Redis.new(options[:redis_options] || {})
9
+
10
+ @namespace = options[:namespace]
11
+ @priority = options[:priority] || 0
12
+
13
+ if options[:use_redis_namespace]
14
+ @connection = ::Redis::Namespace.new(Smooth.namespace, redis: redis)
15
+ else
16
+ @connection = redis
17
+ end
18
+ end
19
+
20
+ def create attributes={}
21
+ attributes.symbolize_keys!
22
+ attributes[:created_at] = attributes[:updated_at] = touch
23
+ attributes[:id] = increment_id
24
+ connection.hset("#{ namespace }:records", attributes[:id], JSON.generate(attributes))
25
+ attributes
26
+ end
27
+
28
+ def update attributes={}
29
+ attributes.symbolize_keys!
30
+ if record = show(attributes[:id])
31
+ record.merge!(attributes)
32
+ record[:updated_at] = touch
33
+ connection.hset("#{ namespace }:records", attributes[:id], JSON.generate(record))
34
+ record
35
+ end
36
+ end
37
+
38
+ def destroy id
39
+ !!(connection.hdel("#{ namespace }:records", id.to_s))
40
+ end
41
+
42
+ def index
43
+ records = connection.hvals("#{ namespace }:records")
44
+ records.map! {|r| JSON.parse(r) }
45
+ records
46
+ end
47
+
48
+ def show id
49
+ record = connection.hget("#{ namespace }:records", id)
50
+ parsed = JSON.parse(record)
51
+ parsed && parsed.symbolize_keys!
52
+ parsed
53
+ end
54
+
55
+ protected
56
+ def maximum_updated_at
57
+ connect.get("#{ namespace }:maximum_updated_at")
58
+ end
59
+
60
+ def touch
61
+ stamp = Time.now.to_i
62
+ connection.set("#{ namespace }:maximum_updated_at", stamp)
63
+ stamp
64
+ end
65
+
66
+ def increment_id
67
+ connection.incr("#{ namespace }:id_incrementer")
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,10 @@
1
+ module Smooth
2
+ module Backends
3
+ class RedisNamespace < Smooth::Backends::Redis
4
+ def initialize options={}
5
+ @namespace, @priority = options.values_at(:namespace, :priority)
6
+ @connection = ::Redis::Namespace.new( @namespace, redis: ::Redis.new(options.fetch(:redis_options, {})))
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,29 @@
1
+ module Smooth
2
+ module Backends
3
+ class RestClient
4
+
5
+ def initialize options={}
6
+
7
+ end
8
+
9
+ def query params={}
10
+ response = Typhoeus::Request.get(url_with_query_string(params))
11
+ end
12
+
13
+ def url
14
+ @options[:url]
15
+ end
16
+
17
+ protected
18
+
19
+ def url_with_query_string params={}
20
+ parts = params.inject([]) do |memo,element|
21
+ key, value = element
22
+ memo << "#{ key }=#{ value }"
23
+ end
24
+
25
+ url + "?#{ parts.join('&') }"
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,236 @@
1
+ module Smooth
2
+ class Collection
3
+ include Cacheable
4
+
5
+ InvalidBackend = Class.new(Exception)
6
+
7
+ attr_reader :options, :backend
8
+
9
+ class << self
10
+ attr_accessor :model_class,
11
+ :backend,
12
+ :namespace
13
+ end
14
+
15
+ def self.uses_model model=Smooth::Model
16
+ self.model_class = model
17
+ self
18
+ end
19
+
20
+ def self.uses_namespace namespace="smooth:collections"
21
+ self.namespace = namespace
22
+ self
23
+ end
24
+
25
+ def self.uses_backend backend=:file
26
+ self.backend = backend
27
+ self
28
+ end
29
+
30
+ # Create an instance of a Smooth::Collection.
31
+ #
32
+ # Examples:
33
+ #
34
+ # Smooth::Collection.new(namespace: "namespace", backend: "file")
35
+ #
36
+ def initialize namespace, models=[], options={}
37
+ if models.is_a?(Hash)
38
+ options = models
39
+ models = Array(options[:models])
40
+ end
41
+
42
+ if namespace.is_a?(Hash)
43
+ options = namespace
44
+ end
45
+
46
+ if namespace.is_a?(String)
47
+ options[:namespace] = namespace
48
+ end
49
+
50
+ @options = options
51
+
52
+ if options[:model] and !options[:backend]
53
+ options[:backend] = "active_record"
54
+ end
55
+
56
+ validate_backend
57
+
58
+ if options[:cache].is_a?(String)
59
+ options[:cache] = {
60
+ backend: "redis"
61
+ }
62
+ end
63
+
64
+ if options[:cache]
65
+ @cache_adapter = create_cache_adapter(options[:cache])
66
+ end
67
+
68
+ @model_class = options[:model_class] || self.class.model_class
69
+
70
+ @models = models
71
+ end
72
+
73
+ def sync method="read", model={}, options={}, &block
74
+
75
+ case method.to_sym
76
+
77
+ when :read
78
+ fetch_from_index
79
+ when :update
80
+ model = data_to_model(model,options)
81
+ backend.update model.as_json
82
+ fetch_from_index
83
+ when :create
84
+ model = data_to_model(model,options)
85
+ attributes = backend.create(model.as_json)
86
+ fetch_from_index
87
+
88
+ model = data_to_model(attributes, options)
89
+ when :destroy
90
+ backend.destroy model.id
91
+ else
92
+ fetch_from_index
93
+ end
94
+ end
95
+
96
+ def add data={}, options={}
97
+ options[:collection] = self
98
+
99
+ model = data_to_model(data, options)
100
+
101
+ @models << model
102
+
103
+ model
104
+ end
105
+
106
+ def find_by attribute, value
107
+ q = {}
108
+ q[attribute.to_sym] = value
109
+ data_to_model query(q).first
110
+ end
111
+
112
+ def find id
113
+ find_by(:id, id)
114
+ end
115
+
116
+ def models
117
+ fetch_from_index unless @fetched
118
+
119
+ Array(@models).map do |model|
120
+ data_to_model(model, collection: self)
121
+ end
122
+ end
123
+
124
+ def url
125
+ backend.url rescue @namespace
126
+ end
127
+
128
+ def query params={}, options={}
129
+ query = Query.new(params)
130
+ query = apply_scope_parameters_to(query)
131
+
132
+ return backend.query(query) unless cacheable?
133
+
134
+ cache_adapter.fetch(query.cache_key) do
135
+ backend.query(query)
136
+ end
137
+ end
138
+
139
+ def fetch options={}
140
+ response = query(options, options[:query_options])
141
+ reset parse(response, options)
142
+ end
143
+
144
+ def parse response, options={}
145
+ response
146
+ end
147
+
148
+ def reset models=nil
149
+ @models = Array(models)
150
+ end
151
+
152
+ def length
153
+ models.length
154
+ end
155
+
156
+ private
157
+ def fetch_from_index
158
+ @fetched = true
159
+ @models = Array(backend && backend.index)
160
+ end
161
+
162
+ def data_to_model data={}, options={}
163
+ data = JSON.parse(data) if data.is_a?(String)
164
+
165
+ begin
166
+ if data.class.ancestors.include?(Smooth::Model)
167
+ model = data
168
+ elsif data.is_a?(Hash)
169
+ model = model_class.new(data, options)
170
+ end
171
+
172
+ model.collection ||= self
173
+
174
+ model
175
+ rescue
176
+ binding.pry if $k
177
+ end
178
+ end
179
+
180
+ def model_class
181
+ @model_class || Smooth::Model
182
+ end
183
+
184
+ def cache_adapter
185
+ @cache_adapter
186
+ end
187
+
188
+ def cacheable?
189
+ !!(options[:cache])
190
+ end
191
+
192
+ def apply_scope_parameters_to query={}
193
+ query
194
+ end
195
+
196
+ def namespace
197
+ namespace = options[:namespace] || self.class.namespace
198
+
199
+ if options[:backend] == "active_record" and options[:model] and namespace.nil?
200
+ namespace = options[:model].table_name
201
+ end
202
+
203
+ namespace
204
+ end
205
+
206
+ def backend_options
207
+ default = {namespace: namespace}
208
+ default[:model] ||= options[:model]
209
+
210
+ default.merge!(backend_options: options.fetch(:backend_options,{}))
211
+ default
212
+ end
213
+
214
+ def configure_backend
215
+ options[:backend] ||= self.class.backend || "file"
216
+ options[:backend] = options[:backend].to_s if options[:backend].is_a?(Symbol)
217
+
218
+ if options[:backend].is_a?(String)
219
+ klass = if options[:backend].match(/::/)
220
+ options[:backend]
221
+ else
222
+ "Smooth::Backends::#{ options[:backend].camelize }"
223
+ end
224
+
225
+ @backend = klass.constantize.new(backend_options)
226
+ end
227
+ end
228
+
229
+ def validate_backend
230
+ configure_backend unless @backend
231
+
232
+ raise InvalidBackend unless backend.respond_to?(:query)
233
+ @backend
234
+ end
235
+ end
236
+ end