smooth 2.0.1 → 2.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +7 -0
  3. data/Gemfile +1 -2
  4. data/README.md +150 -5
  5. data/Rakefile +16 -0
  6. data/app/assets/javascripts/smooth/index.js +5152 -0
  7. data/bin/smooth +9 -0
  8. data/{app/assets/javascripts/smooth → developer-tools}/.keep +0 -0
  9. data/developer-tools/bower.json +8 -0
  10. data/developer-tools/config.ru +3 -0
  11. data/developer-tools/dist/08d606864d3ad3f0b98660d391f5a1c2.gif +0 -0
  12. data/developer-tools/dist/2d66bcdc27cd89f71068e98a7a929712.gif +0 -0
  13. data/developer-tools/dist/3e9816417b11485d454f9b3662b06e7b.eot +0 -0
  14. data/developer-tools/dist/47de617fd1d745ad120ccb9e2924b98c.gif +0 -0
  15. data/developer-tools/dist/5ae23ad29b67289a1375d2043e289c52.eot +0 -0
  16. data/developer-tools/dist/60c2a8500e63bf211b7df9608f7613ea.svg +450 -0
  17. data/developer-tools/dist/645f50ba6c1e56f078fa018855d97eb0.gif +0 -0
  18. data/developer-tools/dist/71ab514d1cedda303417ad7a06472fea.ttf +0 -0
  19. data/developer-tools/dist/8cca2f02b0af2da365ff4d1755f29146.ttf +0 -0
  20. data/developer-tools/dist/939cf252f0eb4efbd2d170c974411c49.gif +0 -0
  21. data/developer-tools/dist/9af25aaeb6ca6d08d213b04841813eb5.gif +0 -0
  22. data/developer-tools/dist/b683029bafe0305ac2234038a03e1541.woff +0 -0
  23. data/developer-tools/dist/c9dec22105ad9330c811599b8b6464f8.woff +0 -0
  24. data/developer-tools/dist/ca279c55a51ab2641c4712a333633581.gif +0 -0
  25. data/developer-tools/dist/client.js +5152 -0
  26. data/developer-tools/dist/f5b27137d3f5e9b1d91b16b37386dd03.gif +0 -0
  27. data/developer-tools/dist/f99a231ed57ee113b50b1c3e9f9fcdc3.svg +399 -0
  28. data/developer-tools/dist/index.html +18 -0
  29. data/developer-tools/dist/inspector.js +38432 -0
  30. data/developer-tools/dist/jquery.min.js +9190 -0
  31. data/developer-tools/package.json +39 -0
  32. data/developer-tools/server.js +14 -0
  33. data/developer-tools/src/client.coffee +21 -0
  34. data/developer-tools/src/client/collection.coffee +14 -0
  35. data/developer-tools/src/client/model.coffee +11 -0
  36. data/developer-tools/src/client/resource.coffee +132 -0
  37. data/{app/controllers/.keep → developer-tools/src/client/runner.coffee} +0 -0
  38. data/developer-tools/src/dependencies.coffee +7 -0
  39. data/developer-tools/src/inspector.cjsx +49 -0
  40. data/developer-tools/src/inspector/models/interface_collection.coffee +31 -0
  41. data/developer-tools/src/inspector/pages/index.cjsx +31 -0
  42. data/developer-tools/src/inspector/pages/resources.cjsx +5 -0
  43. data/developer-tools/src/inspector/views/grid_sort.cjsx +23 -0
  44. data/developer-tools/src/inspector/views/icon_heading.cjsx +15 -0
  45. data/developer-tools/src/inspector/views/resource_card.cjsx +34 -0
  46. data/developer-tools/src/inspector/views/sidebar.cjsx +12 -0
  47. data/developer-tools/src/inspector/views/toolbar.cjsx +17 -0
  48. data/developer-tools/src/styles/index.scss +136 -0
  49. data/developer-tools/src/styles/views.scss +13 -0
  50. data/developer-tools/src/util.coffee +48 -0
  51. data/developer-tools/webpack.config.js +56 -0
  52. data/developer-tools/webpack.hot.config.js +65 -0
  53. data/lib/smooth.rb +209 -28
  54. data/lib/smooth/active_record/adapter.rb +24 -0
  55. data/lib/smooth/api.rb +272 -18
  56. data/lib/smooth/api/policy.rb +2 -2
  57. data/lib/smooth/api/tracking.rb +4 -4
  58. data/lib/smooth/application.rb +66 -0
  59. data/lib/smooth/cache.rb +1 -1
  60. data/lib/smooth/command.rb +267 -18
  61. data/lib/smooth/command/async_worker.rb +27 -0
  62. data/lib/smooth/command/instrumented.rb +6 -4
  63. data/lib/smooth/command/run_proxy.rb +21 -0
  64. data/lib/smooth/configuration.rb +63 -8
  65. data/lib/smooth/documentation.rb +3 -6
  66. data/lib/smooth/dsl.rb +1 -36
  67. data/lib/smooth/dsl_adapter.rb +34 -0
  68. data/lib/smooth/event.rb +8 -4
  69. data/lib/smooth/event/proxy.rb +9 -0
  70. data/lib/smooth/event/relay.rb +38 -0
  71. data/lib/smooth/example.rb +1 -1
  72. data/lib/smooth/ext/core.rb +16 -0
  73. data/lib/smooth/model_adapter.rb +31 -0
  74. data/lib/smooth/query.rb +143 -13
  75. data/lib/smooth/resource.rb +227 -52
  76. data/lib/smooth/resource/router.rb +217 -0
  77. data/lib/smooth/resource/templating.rb +62 -0
  78. data/lib/smooth/resource/tracking.rb +1 -1
  79. data/lib/smooth/response.rb +73 -0
  80. data/lib/smooth/serializer.rb +102 -11
  81. data/lib/smooth/user_adapter.rb +83 -0
  82. data/lib/smooth/util.rb +17 -0
  83. data/lib/smooth/version.rb +1 -1
  84. data/smooth.gemspec +6 -2
  85. data/spec/acceptance/books_routes_spec.rb +50 -0
  86. data/spec/acceptance/embedded_relationships_spec.rb +26 -0
  87. data/spec/dummy/app/apis/application_api.rb +8 -3
  88. data/spec/dummy/app/commands/create_book.rb +5 -0
  89. data/spec/dummy/app/models/book.rb +1 -0
  90. data/spec/dummy/app/models/library.rb +2 -0
  91. data/spec/dummy/app/models/user.rb +2 -0
  92. data/spec/dummy/app/queries/book_query.rb +13 -0
  93. data/spec/dummy/app/resources/{books.rb → books_definition.rb} +37 -12
  94. data/spec/dummy/db/migrate/20140824215902_create_users.rb +10 -0
  95. data/spec/dummy/db/migrate/20140826193259_create_libraries.rb +10 -0
  96. data/spec/dummy/db/schema.rb +8 -1
  97. data/spec/lib/smooth/api/async_spec.rb +21 -0
  98. data/spec/lib/smooth/api_spec.rb +8 -0
  99. data/spec/lib/smooth/command_spec.rb +87 -6
  100. data/spec/lib/smooth/configuration_spec.rb +4 -0
  101. data/spec/lib/smooth/event/relay_spec.rb +33 -0
  102. data/spec/lib/smooth/event_spec.rb +5 -8
  103. data/spec/lib/smooth/query_spec.rb +42 -0
  104. data/spec/lib/smooth/resource/router_spec.rb +14 -0
  105. data/spec/lib/smooth/resource_spec.rb +33 -1
  106. data/spec/lib/smooth/serializer_spec.rb +20 -0
  107. data/spec/lib/smooth/templating_spec.rb +23 -0
  108. data/spec/lib/smooth/util_spec.rb +22 -0
  109. data/spec/spec_helper.rb +1 -1
  110. metadata +151 -17
  111. data/app/helpers/.keep +0 -0
  112. data/app/mailers/.keep +0 -0
  113. data/app/models/.keep +0 -0
  114. data/app/views/.keep +0 -0
  115. data/spec/dummy/db/development.sqlite3 +0 -0
  116. data/spec/dummy/db/test.sqlite3 +0 -0
@@ -0,0 +1,24 @@
1
+ module Smooth
2
+ module AR
3
+ class Adapter
4
+ class << self
5
+ def configure
6
+ end
7
+
8
+ def in_use?
9
+ connection.in_use?
10
+ rescue ActiveRecord::ConnectionNotEstablished
11
+ false
12
+ end
13
+
14
+ def connection
15
+ ActiveRecord::Base.connection
16
+ end
17
+
18
+ def establish_connection
19
+ @connection = ActiveRecord::Base.establish_connection(Smooth.config.active_record)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,11 +1,21 @@
1
- require 'smooth/dsl'
2
-
1
+ # The Smooth API
2
+ #
3
+ # An API is a collection of resources. A Resource is a collection of data models, and the API
4
+ # allows us to run queries against those data models or to run commands that express an intent
5
+ # to mutate the data models.
6
+ #
7
+ # An API provides different methods of authentication, and different policies for authorizations.
8
+ #
9
+ # An API is very easy to put a rest interface in front of, but can also work in other scenarios
10
+ # that speak JSON since the interface pretty well encapsulates the behavior.
3
11
  module Smooth
4
12
  class Api
13
+ # Being able to inspect an API and produce data suitable for generating interface
14
+ # documentation, and automated tests, among other things, is a key feature of the gem.
5
15
  include Smooth::Documentation
6
16
 
7
17
  def self.default
8
- @default ||= Smooth::Api.new
18
+ @default ||= Smooth::Api.new(:default)
9
19
  end
10
20
 
11
21
  attr_accessor :name,
@@ -13,20 +23,231 @@ module Smooth
13
23
  :_resources,
14
24
  :_policies
15
25
 
16
- def initialize(name, options={})
17
- @name = name
26
+ def initialize(name, options = {})
27
+ @name = name.to_s
18
28
  @options = options
19
29
 
20
30
  @_resources = {}
21
31
  @_policies = {}
22
32
  end
23
33
 
24
- def version config=nil
34
+ # The Smooth API is a gateway for the commands and queries
35
+ # that can be run by users against its resources
36
+ def lookup(path)
37
+ lookup_object_by(path)
38
+ end
39
+
40
+ # The Smooth API Provides a Rack Compatible interface
41
+ # so we can mount in sinatra or rails or whatever
42
+ def call(env)
43
+ sinatra.call(env)
44
+ end
45
+
46
+ # All Actions taken against the Smooth API are run 'as'
47
+ # some current user. Example:
48
+ #
49
+ # Running a command:
50
+ #
51
+ # api.as(jonathan).i_would_like_to
52
+ # .run_command("books.create").with(title:'Sup boo')
53
+ #
54
+ # Running a query
55
+ #
56
+ # api.as(soederpop).i_would_like_to
57
+ # .query("books").with(subject:"how to...")
58
+ #
59
+ def as(current_user, &block)
60
+ proxy = DslProxy.new(current_user, self)
61
+ proxy.instance_eval(&block) if block_given?
62
+ proxy
63
+ end
64
+
65
+ # The Smooth API generates a sinatra app to be able to
66
+ # the various resources and run commands, queries, etc.
67
+ def sinatra
68
+ app = @sinatra_application_klass ||= Class.new(Sinatra::Base)
69
+
70
+ @sinatra ||= begin
71
+ _resources.each do |_name, resource|
72
+ resource.router && resource.router.apply_to(app)
73
+ end
74
+
75
+ expose_interface_documentation_via(app)
76
+
77
+ app
78
+ end
79
+ end
80
+
81
+ def inspect
82
+ "Smooth API: #{ name } Resources: #{ resource_names }"
83
+ end
84
+
85
+ # The API will rely on the configured authentication method
86
+ # to determine who the user is. Given some request params
87
+ # and request headers
88
+ def lookup_current_user(params, headers)
89
+ auth_strategy, key = authentication_strategy
90
+
91
+ case
92
+ when auth_strategy == :param && parts = params[key]
93
+ user_class.find_for_token_authentication(parts)
94
+ when auth_strategy == :header && parts = headers[key]
95
+ user_class.find_for_token_authentication(parts)
96
+ else
97
+ user_class.anonymous(params, headers)
98
+ end
99
+ end
100
+
101
+ # The Policy will provide an ability file that we can
102
+ # run a user though. The Policy can be overridden by the
103
+ # resource, too. A policy will pass an object path
104
+ def lookup_policy(_params, _headers)
105
+ {}.to_mash
106
+ # TODO
107
+ #
108
+ # Implement:
109
+ #
110
+ # I think Smooth replaces too much of cancan to rely on it.
111
+ #
112
+ # I think the model where the resource inherits from the api, and
113
+ # the api policy just white lists or black lists commands for given
114
+ # user roles, will be sufficient
115
+ end
116
+
117
+ # The Smooth API provides an Asynchronous interface.
118
+ def perform_async(object_path, payload = {})
119
+ worker.perform_async serialize_for_async(object_path, payload)
120
+ end
121
+
122
+ # Takes a request to do something and serializes the arguments in
123
+ # the memory store. The request will be dispatched to the background job
124
+ # handler and then resumed with the same arguments.
125
+ #
126
+ # Note: Rails Global ID will be a good replacement for this
127
+ def serialize_for_async(object_path, payload)
128
+ key = "#{ name }".parameterize + ":cmd:#{ String.random_token(16) }"
129
+
130
+ request = {
131
+ api: name,
132
+ object_path: object_path,
133
+ payload: payload
134
+ }
135
+
136
+ Smooth.config.memory_store.write(key, request)
137
+
138
+ key
139
+ end
140
+
141
+ # Look up object by path. Used to route requests to
142
+ # commands or queries.
143
+ #
144
+ # Example:
145
+ #
146
+ # lookup('books.create') #=> CreateBook
147
+ def lookup_object_by(path)
148
+ path = path.to_s
149
+ resource_name, object_name = path.split(Smooth.config.object_path_separator)
150
+
151
+ resource_object = resource(resource_name)
152
+
153
+ case
154
+ when object_name == 'query' || object_name == 'serializer'
155
+ resource_object.fetch(object_name.to_sym, :default)
156
+ when object_name.nil?
157
+ resource_object
158
+ else
159
+ resource_object.fetch(:command, object_name)
160
+ end
161
+ end
162
+
163
+ def resource_keys
164
+ _resources.keys
165
+ end
166
+
167
+ def resource_names
168
+ _resources.values.map(&:resource_name).compact
169
+ end
170
+
171
+ def resource_group_names
172
+ _resources.values.map(&:group_description).compact
173
+ end
174
+
175
+ def documentation_base
176
+ {
177
+ api_meta: {
178
+ resource_names: resource_names,
179
+ resource_groups: resource_group_names
180
+ }
181
+ }
182
+ end
183
+
184
+ def interface_documentation
185
+ resource_keys.reduce(documentation_base) do |memo, key|
186
+ memo.tap do
187
+ if resource = resource(key)
188
+ memo[resource.resource_name || key.to_s] = resource.interface_documentation
189
+ end
190
+ end
191
+ end
192
+ end
193
+
194
+ def expose_interface_documentation_via(sinatra)
195
+ api = self
196
+
197
+ sinatra.send :get, '/interface' do
198
+ api.interface_documentation.to_json
199
+ end
200
+
201
+ sinatra.send :get, '/interface/:resource_name' do
202
+ docs = api.interface_documentation[params[:resource_name]]
203
+ docs.to_json
204
+ end
205
+ end
206
+
207
+ def version(config = nil)
25
208
  @_version_config = config if config
26
209
  @_version_config
27
210
  end
28
211
 
29
- def policy policy_name, options={}, &block
212
+ def user_class(user_klass = nil, &block)
213
+ @user_class = user_klass if user_klass.present?
214
+ @user_class || User
215
+ @user_class.class_eval(&block) if block_given?
216
+ @user_class
217
+ end
218
+
219
+ def authentication_strategy(option = nil, key = nil)
220
+ return @authentication_strategy || [:header, 'X-AUTH-TOKEN'] if option.nil?
221
+
222
+ unless option.nil?
223
+ key = case
224
+ when key.present?
225
+ key
226
+ when option.to_sym == :param
227
+ :auth_token
228
+ when option.to_sym == :header
229
+ 'X-AUTH-TOKEN'
230
+ end
231
+ end
232
+
233
+ @authentication_strategy = [option, key]
234
+ end
235
+
236
+ def worker(&block)
237
+ worker_name = "#{ name }".camelize + 'Worker'
238
+
239
+ if worker_klass = Smooth::Api.const_get(worker_name) rescue nil
240
+ @worker_klass = worker_klass
241
+ else
242
+ Object.const_get(worker_name, @worker_klass = Class.new(Smooth::Command::AsyncWorker))
243
+ end
244
+
245
+ @worker_klass.instance_eval(&block)
246
+
247
+ @worker_klass
248
+ end
249
+
250
+ def policy(policy_name, options = {}, &block)
30
251
  if obj = _policies[policy_name.to_sym]
31
252
  obj.apply_options(options) unless options.empty?
32
253
  obj.instance_eval(&block) if block_given?
@@ -37,22 +258,23 @@ module Smooth
37
258
 
38
259
  elsif block_given?
39
260
  obj = Smooth::Api::Policy.new(policy_name, options, &block)
40
- _resources[policy_name.to_sym] = obj
261
+ _policies[policy_name.to_sym] = obj
41
262
  end
42
263
  end
43
264
 
44
- def has_resource? resource_name
45
- resources.has_key?(resource_name.to_sym)
265
+ def has_resource?(resource_name)
266
+ resources.key?(resource_name.to_sym)
46
267
  end
47
268
 
48
- def resource resource_name, options={}, &block
49
- api_name = self.name
269
+ def resource(resource_name, options = {}, &block)
270
+ api_name = name
50
271
 
51
- if existing = _resources[resource_name.to_sym]
272
+ existing = _resources[resource_name.to_s.downcase]
273
+
274
+ if existing
52
275
  existing.apply_options(options) unless options.empty?
53
276
  existing.instance_eval(&block) if block_given?
54
277
  existing
55
-
56
278
  elsif options.empty? && !block_given?
57
279
  existing = nil
58
280
 
@@ -61,12 +283,44 @@ module Smooth
61
283
  obj.api_name = api_name
62
284
  end
63
285
 
64
- _resources[resource_name.to_sym] = created
286
+ _resources[resource_name.to_s.downcase] = created
65
287
  end
66
288
  end
289
+ end
290
+
291
+ class DslProxy
292
+ def initialize(current_user, api)
293
+ @current_user = current_user
294
+ @api = api
295
+ end
296
+
297
+ def i_would_like_to
298
+ self
299
+ end
67
300
 
301
+ def lemme
302
+ self
303
+ end
304
+
305
+ def imll
306
+ self
307
+ end
308
+
309
+ def query(resource_name, *args)
310
+ params = args.extract_options!
311
+ query_name = args.first || :default
312
+ runner = @api.resource(resource_name).fetch(:query, query_name).as(@current_user)
313
+ runner.async? ? perform_async(runner.object_path, params) : runner.run(params)
314
+ end
315
+
316
+ def run_command(resource_name, *args)
317
+ params = args.extract_options!
318
+ command_name = args.first
319
+ path = resource_name if command_name.nil?
320
+ path = "#{ resource_name }.#{ command_name }" if command_name.present?
321
+
322
+ runner = @api.lookup_object_by(path).as(@current_user)
323
+ runner.async? ? perform_async(runner.object_path, params) : runner.run(params)
324
+ end
68
325
  end
69
326
  end
70
-
71
- require 'smooth/api/tracking'
72
- require 'smooth/api/policy'
@@ -5,12 +5,12 @@ module Smooth
5
5
 
6
6
  attr_accessor :name
7
7
 
8
- def initialize name, options={}
8
+ def initialize(name, options = {})
9
9
  @name = name
10
10
  @options = options
11
11
  end
12
12
 
13
- def apply_options *opts
13
+ def apply_options(*opts)
14
14
  @options.send(:merge!, *opts)
15
15
  end
16
16
  end
@@ -5,7 +5,7 @@ module Smooth
5
5
  @@apis ||= {}
6
6
  end
7
7
 
8
- def fetch_api name, &block
8
+ def fetch_api(name, &block)
9
9
  existing = apis[name.to_sym]
10
10
 
11
11
  if existing.nil? && block_given?
@@ -16,15 +16,15 @@ module Smooth
16
16
  end
17
17
 
18
18
  def current_api
19
- apis[current_api_name] ||= Smooth::Api.default()
19
+ apis[current_api_name] ||= Smooth::Api.default
20
20
  end
21
21
 
22
- def current_api_name= value
22
+ def current_api_name=(value)
23
23
  @@current_api_name = value
24
24
  end
25
25
 
26
26
  def current_api_name
27
- (@@current_api_name || :default).to_sym
27
+ (@@current_api_name ||= :default).to_sym
28
28
  end
29
29
  end
30
30
  end
@@ -0,0 +1,66 @@
1
+ module Smooth
2
+ class Application
3
+ attr_reader :options
4
+
5
+ class User < Hashie::Mash
6
+ end
7
+
8
+ def initialize(options = {}, &block)
9
+ @options = options
10
+ instance_eval(&block) if block_given?
11
+
12
+ config do
13
+ self.root = options[:root]
14
+ end
15
+
16
+ boot unless options[:defer]
17
+ end
18
+
19
+ def config(&block)
20
+ Smooth.config(&block)
21
+ end
22
+
23
+ def console
24
+ require 'pry'
25
+ Pry.start(self, {})
26
+ end
27
+
28
+ def system_user
29
+ @system_user ||= User.new(email: 'system@smooth.io', role: 'system')
30
+ end
31
+
32
+ def api
33
+ @api ||= Smooth(options[:api] || :default)
34
+ end
35
+
36
+ def smooth
37
+ @smooth ||= api.as(system_user)
38
+ end
39
+
40
+ def resource(*args, &_block)
41
+ api.send(:resource, *args)
42
+ end
43
+
44
+ def query(*args)
45
+ smooth.send(:query, *args)
46
+ end
47
+
48
+ def command(*args)
49
+ smooth.send(:run_command, *args)
50
+ end
51
+
52
+ def load_models
53
+ Dir[config.models_path.join('**/*.rb')].each do |f|
54
+ require config.models_path.join(f)
55
+ end
56
+ end
57
+
58
+ def boot
59
+ @boot ||= begin
60
+ Smooth.active_record.establish_connection
61
+ load_models
62
+ Smooth.eager_load_from_app_folders
63
+ end
64
+ end
65
+ end
66
+ end