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
@@ -8,7 +8,7 @@ module Smooth
8
8
  class Cache
9
9
  include Singleton
10
10
 
11
- def method_missing meth, *args, &block
11
+ def method_missing(meth, *args, &block)
12
12
  if defined? ::Rails
13
13
  return ::Rails.cache.send(meth, *args, &block)
14
14
  end
@@ -1,35 +1,131 @@
1
- require 'smooth/ext/mutations'
2
- require 'smooth/command/instrumented'
3
-
4
1
  class Smooth::Command < Mutations::Command
5
2
  include Instrumented
6
3
 
4
+ def self.as(current_user)
5
+ require 'smooth/command/run_proxy' unless defined?(RunProxy)
6
+ RunProxy.new(current_user, self)
7
+ end
8
+
9
+ # DSL Improvements English
10
+ def self.params(*args, &block)
11
+ send(:required, *args, &block)
12
+ end
13
+
14
+ def self.interface(*args, &block)
15
+ send(:required, *args, &block)
16
+ end
17
+
18
+ def self.input_argument_names
19
+ required_inputs.keys + optional_inputs.keys
20
+ end
21
+
22
+ # Commands are aware of who is running them
23
+ attr_accessor :current_user
24
+
7
25
  class_attribute :resource_name,
8
26
  :command_action,
9
- :event_namespace
27
+ :event_namespace,
28
+ :model_class,
29
+ :base_scope,
30
+ :parent_resource
10
31
 
11
- def self.scope setting
12
- @@scope = setting
32
+ def self.base_scope
33
+ @base_scope || :all
13
34
  end
14
35
 
15
- def self.params *args, &block
16
- send(:required, *args, &block)
36
+ def parent_api
37
+ self.class.parent_api
38
+ end
39
+
40
+ def parent_resource
41
+ self.class.parent_resource
42
+ end
43
+
44
+ def self.belongs_to_resource(resource)
45
+ self.parent_resource = resource
46
+ end
47
+
48
+ def self.parent_api
49
+ parent_resource.api
50
+ end
51
+
52
+ # Returns the model scope for this command. If a scope method
53
+ # is set on this command, it will make sure to scope the model
54
+ # by that method. It will pass whatever arguments you pass to scope
55
+ # to the scope method. if you pass no args, and the scope requires one,
56
+ # we will assume the user wants us to pass the current user of the command
57
+ def scope(*args)
58
+ @scope ||= begin
59
+ meth = model_class.send(:method, self.class.base_scope)
60
+
61
+ if meth.arity.abs >= 1
62
+ args.push(current_user) if args.empty?
63
+ model_class.send(self.class.base_scope, *args)
64
+ else
65
+ model_class.send(self.class.base_scope)
66
+ end
67
+ end
68
+ end
69
+
70
+ def scope=(new_scope)
71
+ @scope = new_scope || scope
72
+ end
73
+
74
+ def self.scope(setting = nil)
75
+ self.base_scope = setting if setting
76
+ base_scope || :all
17
77
  end
18
78
 
19
79
  def self.event_namespace
20
- @event_namespace || "#{ command_action }.#{ resource_name.singularize.underscore }".downcase
80
+ @event_namespace || "#{ command_action }.#{ resource_alias }".downcase
21
81
  end
22
82
 
23
- def event_namespace; self.class.event_namespace; end
83
+ def self.resource_alias
84
+ resource_name.singularize.underscore
85
+ end
86
+
87
+ def self.resource_name
88
+ value = @resource_name.to_s
89
+
90
+ if value.empty? && model_class
91
+ value = model_class.to_s
92
+ end
93
+
94
+ value
95
+ end
96
+
97
+ def self.object_path
98
+ resource_name.downcase + '.' + command_action
99
+ end
100
+
101
+ def event_namespace
102
+ self.class.event_namespace
103
+ end
104
+
105
+ def object_path
106
+ self.class.object_path
107
+ end
108
+
109
+ def resource_name
110
+ self.class.resource_name
111
+ end
112
+
113
+ def resource_alias
114
+ self.class.resource_alias
115
+ end
116
+
117
+ def model_class
118
+ self.class.model_class
119
+ end
24
120
 
25
121
  # DSL Hooks
26
122
  #
27
123
  #
28
- def self.configure options, resource=nil
124
+ def self.configure(dsl_config_object, resource = nil)
29
125
  resource ||= Smooth.current_resource
30
- klass = define_or_open(options, resource)
126
+ klass = define_or_open(dsl_config_object, resource)
31
127
 
32
- Array(options.blocks).each do |blk|
128
+ Array(dsl_config_object.blocks).each do |blk|
33
129
  klass.class_eval(&blk)
34
130
  end
35
131
 
@@ -41,15 +137,168 @@ class Smooth::Command < Mutations::Command
41
137
  base = Smooth.command
42
138
 
43
139
  name = options.name.to_s.camelize
44
- klass = "#{ name }#{ resource_name }"
140
+ klass = "#{ name }#{ resource.model_class }".gsub(/\s+/, '')
141
+
142
+ apply_options = lambda do |k|
143
+ k.model_class ||= resource.model_class if resource.model_class
144
+
145
+ k.belongs_to_resource(resource)
146
+
147
+ k.resource_name = resource.name.to_s
148
+ k.command_action = options.name.to_s
149
+ end
45
150
 
46
151
  if command_klass = Object.const_get(klass) rescue nil
47
- return command_klass
152
+ return command_klass.tap(&apply_options)
48
153
  end
49
154
 
50
- Object.const_set(klass, Class.new(base)).tap do |k|
51
- k.resource_name = resource.name.to_s
52
- k.command_action = options.name.to_s
155
+ parent_klass = Class.new(base)
156
+
157
+ begin
158
+ Object.const_set(klass, parent_klass).tap(&apply_options)
159
+ rescue => ex
160
+ puts ex.message
161
+ puts "Error setting #{ klass } #{ base }. klass is a #{ klass.class }"
162
+ end
163
+
164
+ parent_klass
165
+ end
166
+
167
+ # Interface Documentation
168
+ #
169
+ def interface_for(filter)
170
+ self.class.interface_description.filters.send(filter)
171
+ end
172
+
173
+ def self.interface_description
174
+ interface_documentation
175
+ end
176
+
177
+ def self.interface_documentation
178
+ optional_inputs = input_filters.optional_inputs
179
+ required_inputs = input_filters.required_inputs
180
+
181
+ data = {
182
+ required: required_inputs.keys,
183
+ optional: optional_inputs.keys,
184
+ filters: {}
185
+ }
186
+
187
+ blk = lambda do |memo, parts, required|
188
+ key, filter = parts
189
+
190
+ type = filter.class.name[/^Mutations::([a-zA-Z]*)Filter$/, 1].underscore
191
+ options = filter.options.merge(required: required)
192
+
193
+ value = memo[key] = {
194
+ type: type,
195
+ options: options.reject { |_k, v| v.nil? },
196
+ description: input_descriptions[key]
197
+ }
198
+
199
+ if options[:faker]
200
+ value[:example] = Smooth.faker(options[:faker])
201
+ end
202
+
203
+ memo
204
+ end
205
+
206
+ required_inputs.reduce(data[:filters]) do |memo, parts|
207
+ blk.call(memo, parts, true)
208
+ end
209
+
210
+ optional_inputs.reduce(data[:filters]) do |memo, parts|
211
+ blk.call(memo, parts, false)
212
+ end
213
+
214
+ data.to_mash
215
+ end
216
+
217
+ def self.filter_for_param(param)
218
+ optional_inputs[param] || required_inputs[param]
219
+ end
220
+
221
+ def self.filter_options_for_param(param)
222
+ filter_for_param(param).try(:options)
223
+ end
224
+
225
+ def self.response_class
226
+ Smooth::Response
227
+ end
228
+
229
+ # Creates a new instance of the Smooth::Command::Response
230
+ # class in response to a request from the Router. It is
231
+ # assumed that a request object responds to: user, and params
232
+ def self.respond_to_request(request_object, options = {})
233
+ klass = self
234
+
235
+ outcome = options.fetch(:outcome) do
236
+ klass.as(request_object.user).run(request_object.params)
237
+ end
238
+
239
+ response_class.new(outcome, serializer_options).tap do |response|
240
+ response.request_headers = request_object.headers
241
+ response.serializer = find_serializer_for(request_object)
242
+ response.event_namespace = event_namespace
243
+ response.command_action = command_action
244
+ response.current_user = request_object.user
245
+ end
246
+ end
247
+
248
+ def self.find_serializer_for(_request_object)
249
+ resource = Smooth.resource(resource_name)
250
+ resource ||= Smooth.resource("#{ resource_name }".pluralize)
251
+
252
+ # TODO
253
+ # We can make the preferred format something you can
254
+ # specify via a header or parameter. We can also restrict
255
+ # certain serializers from certain policies.
256
+ preferred_format = :default
257
+
258
+ resource.fetch(:serializer, preferred_format)
259
+ end
260
+
261
+ def self.serializer_options
262
+ {}
263
+ end
264
+
265
+ # Allows for defining common execution pattern methods
266
+ # mostly for standard CRUD against scoped models
267
+ def self.execute(execution_pattern = nil, &block)
268
+ send :define_method, :execute, (block || get_execution_pattern(execution_pattern))
269
+ end
270
+
271
+ Patterns = {}
272
+
273
+ Patterns[:update] = lambda do
274
+ scoped_find_object.update_attributes(inputs)
275
+ scoped_find_object
276
+ end
277
+
278
+ Patterns[:create] = lambda do
279
+ scope.create(inputs)
280
+ end
281
+
282
+ Patterns[:destroy] = lambda do
283
+ scoped_find_object.destroy
284
+ scoped_find_object
285
+ end
286
+
287
+ def scoped_find_object
288
+ @scoped_find ||= if scope.respond_to?(:find) && found = (scope.find(id) rescue nil)
289
+ found
290
+ else
291
+ add_error(:id, :not_found, "could not find a #{ resource_name } model with that id")
292
+ end
293
+ end
294
+
295
+ def self.get_execution_pattern(pattern_name)
296
+ if respond_to?("#{ pattern_name }_execution_pattern")
297
+ return method("#{ pattern_name }_execution_pattern").to_proc
298
+ elsif Patterns.key?(pattern_name.to_sym)
299
+ Patterns.fetch(pattern_name.to_sym)
300
+ else
301
+ return method(:execute).to_proc
53
302
  end
54
303
  end
55
304
  end
@@ -0,0 +1,27 @@
1
+ module Smooth
2
+ class Command::AsyncWorker
3
+ unless Smooth.config.async_provider
4
+ fail 'Must specify an async provider. e.g. Sidekiq::Worker on Smooth.config.async_provider'
5
+ end
6
+
7
+ def self.options(*args)
8
+ send(:sidekiq_options, *args) if defined?(Sidekiq)
9
+ end
10
+
11
+ def perform(serialized_payload)
12
+ if hash = memory_store.read(serialized_payload)
13
+ api, object_path, payload = hash.values_at('api', 'object_path', 'payload')
14
+ current_user = payload['current_user'] || hash['current_user']
15
+
16
+ chain = Smooth(api).lookup_object_by(object_path)
17
+ chain = chain.as(current_user) if current_user
18
+
19
+ chain.run(payload)
20
+ end
21
+ end
22
+
23
+ def memory_store
24
+ Smooth.config.memory_store
25
+ end
26
+ end
27
+ end
@@ -29,15 +29,17 @@ module Smooth
29
29
  end
30
30
  end
31
31
 
32
- def run_with_instrumentation
32
+ def run_with_instrumentation(event_prefix = nil)
33
33
  outcome = run_with_outcome
34
34
 
35
+ event_prefix = "#{ event_prefix }." if event_prefix
36
+
35
37
  if outcome.success?
36
38
  result = outcome.result
37
- track_event("#{ event_namespace }", result: result, inputs: inputs)
38
- result
39
+ track_event("#{ event_prefix }#{ event_namespace }", result: result, inputs: inputs, current_user: current_user)
40
+ outcome
39
41
  else
40
- track_event("errors/#{ event_namespace }", errors: outcome.errors, inputs: inputs)
42
+ track_event("errors/#{ event_prefix }#{ event_namespace }", errors: outcome.errors, inputs: inputs, current_user: current_user)
41
43
  outcome
42
44
  end
43
45
  end
@@ -0,0 +1,21 @@
1
+ # Internal class to provide current user awareness to the
2
+ module Smooth
3
+ class Command < Mutations::Command
4
+ class RunProxy
5
+ attr_accessor :current_user, :cmd
6
+
7
+ def initialize(current_user, cmd)
8
+ @current_user = current_user
9
+ @cmd = cmd
10
+ end
11
+
12
+ def run!(*args)
13
+ cmd.new(*args).tap { |c| c.current_user = current_user }.run!
14
+ end
15
+
16
+ def run(*args)
17
+ cmd.new(*args).tap { |c| c.current_user = current_user }.run
18
+ end
19
+ end
20
+ end
21
+ end
@@ -1,5 +1,3 @@
1
- require 'singleton'
2
-
3
1
  module Smooth
4
2
  class Configuration
5
3
  include Singleton
@@ -7,18 +5,75 @@ module Smooth
7
5
  cattr_accessor :query_class,
8
6
  :command_class,
9
7
  :serializer_class,
10
- :enable_events
8
+ :object_path_separator,
9
+ :enable_events,
10
+ :definition_folders,
11
+ :eager_load_app_folders,
12
+ :active_record_config,
13
+ :models_path,
14
+ :schema_file,
15
+ :migrations_path,
16
+ :root,
17
+ :include_root_in_json,
18
+ :auth_token_column,
19
+ :enable_factories,
20
+ :async_provider,
21
+ :memory_store,
22
+ :embed_relationships_as
23
+
24
+ @@query_class = Smooth::Query
25
+ @@command_class = Smooth::Command
26
+ @@serializer_class = defined?(ApplicationSerializer) ? ApplicationSerializer : Smooth::Serializer
27
+ @@enable_events = true
28
+ @@eager_load_app_folders = true
29
+ @@models_path = 'app/models'
30
+ @@object_path_separator = '.'
31
+ @@definition_folders = %w(app/models app/apis app/queries app/commands app/serializers app/resources)
32
+ @@include_root_in_json = true
33
+ @@enable_factories = true
11
34
 
12
- @@query_class = Smooth::Query
13
- @@command_class = Smooth::Command
14
- @@serializer_class = defined?(ApplicationSerializer) ? ApplicationSerializer : Smooth::Serializer
15
- @@enable_events = true
35
+ @@active_record_config = 'config/database.yml'
36
+ @@schema_file = 'db/schema.rb'
37
+ @@migrations_path = 'db/migrate'
38
+ @@root = Dir.pwd
39
+ @@auth_token_column = :authentication_token
40
+ @@async_provider = Sidekiq::Worker if defined?(Sidekiq)
41
+ @@memory_store = Smooth.cache
42
+
43
+ @@embed_relationships_as = :ids
44
+
45
+ def active_record
46
+ return active_record_config if active_record_config.is_a?(Hash)
47
+ file = root.join(active_record_config)
48
+ fail 'The config file does not exist at ' + file.to_s unless file.exist?
49
+ YAML.load(file.open).fetch(Smooth.env)
50
+ end
16
51
 
17
52
  def enable_event_tracking?
18
53
  !!@@enable_events
19
54
  end
20
55
 
21
- def self.method_missing meth, *args, &block
56
+ def root
57
+ Pathname(@@root)
58
+ end
59
+
60
+ def app_folder_paths
61
+ Array(definition_folders).map { |f| root.join(f) }
62
+ end
63
+
64
+ def models_path
65
+ root.join(@@models_path)
66
+ end
67
+
68
+ def method_missing(meth, *args, &block)
69
+ if meth.to_s.match(/(\w+)\?$/)
70
+ !!(send(Regexp.last_match[1], *args, &block)) if respond_to?(Regexp.last_match[1])
71
+ else
72
+ super
73
+ end
74
+ end
75
+
76
+ def self.method_missing(meth, *args, &block)
22
77
  instance.send(meth, *args, &block)
23
78
  end
24
79
  end