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,217 @@
1
+ module Smooth
2
+ class Resource
3
+ class Router
4
+ attr_reader :resource,
5
+ :table,
6
+ :descriptions,
7
+ :rules
8
+
9
+ def initialize(resource, _options = {})
10
+ @resource = resource
11
+ @table = {}
12
+ @descriptions = {}
13
+ @rules = []
14
+ end
15
+
16
+ # I may be getting this in a convoluted way
17
+ # may be easier to build up naturally
18
+ def interface_documentation
19
+ descriptions.keys.reduce({}) do |memo, verb|
20
+ routes = descriptions[verb]
21
+ routes.each do |_|
22
+ pattern, description = _
23
+ memo["#{ verb.to_s.upcase } #{ pattern }"] = description
24
+ end
25
+
26
+ memo
27
+ end
28
+ end
29
+
30
+ def route_table
31
+ @route_table ||= route_patterns_table.reduce({}) do |memo, p|
32
+ route_name, details = p
33
+ memo[route_name] = details[:pattern]
34
+ memo
35
+ end
36
+ end
37
+
38
+ def expand_routes(from_attributes = {})
39
+ route_patterns_table.reduce({}) do |memo, p|
40
+ route_name, details = p
41
+ memo[route_name] = Smooth.util.expand_url_template(details[:template], from_attributes)
42
+ memo
43
+ end
44
+ end
45
+
46
+ def route_patterns_table
47
+ return @route_patterns_table if @route_patterns_table
48
+
49
+ @route_patterns_table = rules.flatten.compact.reduce({}) do |memo, rule|
50
+ memo.tap do
51
+ name = rule[:name]
52
+ pattern = rule[:pattern]
53
+ template = rule[:template]
54
+
55
+ memo[name] = {
56
+ pattern: pattern,
57
+ template: template,
58
+ variables: Array(template.variables)
59
+ }
60
+ end
61
+ end
62
+ end
63
+
64
+ def patterns
65
+ rules.flatten.compact.map { |r| r.fetch(:pattern) }
66
+ end
67
+
68
+ def uri_templates
69
+ rules.flatten.compact.map { |r| r.fetch(:template) }
70
+ end
71
+
72
+ def apply_to(sinatra)
73
+ router = self
74
+
75
+ user_finder = resource.api.method(:lookup_current_user).to_proc
76
+ policy_finder = resource.api.method(:lookup_policy).to_proc
77
+
78
+ router.rules.each do |_|
79
+ options, _ = _
80
+
81
+ handler = methods_table.method(options[:name])
82
+
83
+ sinatra.send options[:method], options[:pattern] do |*args|
84
+ begin
85
+ request = {
86
+ headers: headers,
87
+ params: params,
88
+ user: user_finder.call(params, headers),
89
+ policy: policy_finder.call(params, headers),
90
+ args: args
91
+ }
92
+ rescue => exception
93
+ halt 500, {}, { error: exception.message, backtrace: exception.backtrace, stage: 'request' }.to_json
94
+ end
95
+
96
+ begin
97
+ response = handler.call(request.to_mash)
98
+
99
+ body response.body
100
+ headers response.headers
101
+ status response.status
102
+ rescue => exception
103
+ halt 500, {}, { error: exception.message, backtrace: exception.backtrace, stage: 'response' }.to_json
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+ def build_methods_table
110
+ router = self
111
+
112
+ @methods_table_class = Class.new do
113
+
114
+ k = self
115
+
116
+ router.rules.each do |_|
117
+ options, block = _
118
+ method_name = options.fetch(:name)
119
+ k.send :define_method, method_name, (block || router.lookup_handler_for(options[:method], options[:to]))
120
+ end
121
+ end
122
+ end
123
+
124
+ def counter
125
+ @counter ||= 0
126
+ @counter += 1
127
+ end
128
+
129
+ def methods_table
130
+ @methods_table ||= (@methods_table_class || build_methods_table).new
131
+ end
132
+
133
+ def desc(description, *_args)
134
+ descriptions[:current] = description
135
+ end
136
+
137
+ Verbs = {
138
+ get: :get,
139
+ show: :get,
140
+ put: :put,
141
+ patch: :put,
142
+ create: :post,
143
+ delete: :delete,
144
+ destroy: :destroy,
145
+ options: :options,
146
+ post: :post
147
+ }
148
+
149
+ def method_missing(meth, *args, &block)
150
+ if Verbs.keys.include?(meth.to_sym)
151
+ pattern = args.shift
152
+ define_route(meth, pattern, *args, &block)
153
+ else
154
+ super
155
+ end
156
+ end
157
+
158
+ def define_route(request_method, route_pattern, *args, &block)
159
+ request_method = Verbs.fetch(request_method.to_sym, :get)
160
+ bucket = table[request_method] ||= {}
161
+ options = args.extract_options!
162
+
163
+ name = options.fetch(:as, "#{ request_method }_#{ counter }")
164
+
165
+ describe_route(request_method, route_pattern)
166
+
167
+ rules << bucket[route_pattern] = [
168
+ options.merge(name: name, method: request_method, args: args, pattern: route_pattern, template: Smooth.util.uri_template(route_pattern)),
169
+ block
170
+ ]
171
+ end
172
+
173
+ def describe_route(request_method, route_pattern)
174
+ documentation = descriptions[request_method] ||= {}
175
+
176
+ if description = descriptions[:current]
177
+ documentation[route_pattern] = description
178
+ descriptions.delete(:current)
179
+ end
180
+ end
181
+
182
+ # Allows for a configuration syntax like
183
+ #
184
+ # routes do
185
+ # get "/books", :to => :query
186
+ # end
187
+ #
188
+ # the lookup_handler_for method will attempt to
189
+ # discern which object is best suited to handle the
190
+ # request based on the http verb and the signifier
191
+ def lookup_handler_for(method, signifier)
192
+ method = method.to_sym
193
+ signifier = signifier.to_sym
194
+
195
+ resource = self.resource
196
+
197
+ case
198
+
199
+ when method == :get && signifier == :query
200
+ ->(req) { resource.fetch(:query, :default).respond_to_request(req) }
201
+
202
+ when (method == :show || method == :get) && signifier == :show
203
+ ->(req) { resource.fetch(:query, :default).respond_to_find_request(req) }
204
+
205
+ when method == :get
206
+ ->(req) { resource.fetch(:query, signifier).respond_to_request(req) }
207
+
208
+ # Mutation Methods
209
+ when method == :put || method == :post || method == :delete
210
+ ->(req) { resource.fetch(:command, signifier).respond_to_request(req) }
211
+ else
212
+ ->(req) { Smooth::ErrorResponse.new('Unable to find matching route', req) }
213
+ end
214
+ end
215
+ end
216
+ end
217
+ end
@@ -0,0 +1,62 @@
1
+ module Smooth
2
+ class Resource
3
+ module Templating
4
+ FakerGroups = %w(
5
+ Address Lorem Color Company Food HipterIpsum Internet Job Name Movie PhoneNumber Product Unit Vehicle Venue Skill
6
+ )
7
+
8
+ def self.fakers
9
+ FakerGroups.flat_map do |group|
10
+ prefix = group.to_s.underscore.downcase
11
+ space = Faker.const_get(group.to_sym) rescue nil
12
+
13
+ if space && space.class == Module
14
+ (space.methods - Object.methods - [:k, :underscore]).map { |m| "#{prefix}.#{m}" }
15
+ end
16
+ end.compact.uniq
17
+ end
18
+
19
+ def create_from_template(name = nil, *args, &block)
20
+ if name.is_a?(Hash)
21
+ args.unshift(name)
22
+ name = @template_name
23
+ end
24
+ FactoryGirl.create(name || @template_name, *args, &block)
25
+ end
26
+
27
+ def build_from_template(name = nil, *args, &block)
28
+ if name.is_a?(Hash)
29
+ args.unshift(name)
30
+ name = @template_name
31
+ end
32
+
33
+ FactoryGirl.build(name || @template_name, *args, &block)
34
+ end
35
+
36
+ def template(name = nil, *args, &block)
37
+ options = args.extract_options!
38
+
39
+ if name.nil?
40
+ name = model_class.table_name.singularize.to_sym
41
+ @template_name ||= name
42
+ end
43
+
44
+ options[:class] ||= model_class
45
+
46
+ FactoryGirl.define do
47
+ factory(name.to_sym, options, &block)
48
+ end
49
+ end
50
+
51
+ def template_registered?(name = nil)
52
+ name ||= model_class.table_name.singularize.to_sym
53
+ !!(FactoryGirl.factory_by_name(name) rescue nil)
54
+ end
55
+
56
+ # Just allows us to wrap template definitions
57
+ def templates(&block)
58
+ instance_eval(&block)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -5,7 +5,7 @@ module Smooth
5
5
  @@resources ||= {}
6
6
  end
7
7
 
8
- def current_resource= resource_object
8
+ def current_resource=(resource_object)
9
9
  @current_resource = resource_object.identifier
10
10
  end
11
11
 
@@ -0,0 +1,73 @@
1
+ module Smooth
2
+ class Response
3
+ attr_reader :outcome, :serializer_options
4
+
5
+ attr_accessor :request_headers, :serializer, :event_namespace, :command_action, :success, :object, :serializer_klass, :current_user
6
+
7
+ def initialize(outcome, serializer_options = {})
8
+ @outcome = outcome
9
+ @serializer_options = serializer_options
10
+ end
11
+
12
+ def to_rack
13
+ [status, headers, [body]]
14
+ end
15
+
16
+ def headers
17
+ {
18
+ 'Content-Type' => 'application/json'
19
+ }
20
+ end
21
+
22
+ def options
23
+ if success? && serializer
24
+ (@serializer_options || {}).merge(serializer: serializer, scope: current_user)
25
+ else
26
+ @serializer_options.merge(scope: current_user)
27
+ end
28
+ end
29
+
30
+ def body
31
+ serializer.new(object, options).to_json(options)
32
+ end
33
+
34
+ def object
35
+ @object || begin
36
+ if success?
37
+ outcome.result
38
+ else
39
+ outcome.errors.message
40
+ end
41
+ end
42
+ end
43
+
44
+ def success?
45
+ @success || (outcome && outcome.success?)
46
+ end
47
+
48
+ def status
49
+ case
50
+ when success?
51
+ 200
52
+ else
53
+ 400
54
+ end
55
+ end
56
+ end
57
+
58
+ class ErrorResponse < Response
59
+ def initialize(error_message, _request_object, *_args)
60
+ @error_message = error_message
61
+ end
62
+
63
+ def success?
64
+ false
65
+ end
66
+
67
+ def body
68
+ {
69
+ error: error_message
70
+ }
71
+ end
72
+ end
73
+ end
@@ -3,12 +3,50 @@ module Smooth
3
3
  include Smooth::Documentation
4
4
 
5
5
  class_attribute :attribute_descriptions,
6
- :relationship_descriptions
6
+ :relationship_descriptions,
7
+ :resource_route_variables
7
8
 
8
9
  self.attribute_descriptions = {}.to_mash
9
10
  self.relationship_descriptions = {}.to_mash
10
11
 
11
- def self.method_added method_name
12
+ # WIP
13
+ # Need to determine how to access the serialized version
14
+ # as it exists in the context of the serializer instance
15
+ def expand_routes(*slice)
16
+ expanded = parent_resource.expand_routes(route_variables)
17
+
18
+ unless slice.empty?
19
+ expanded = expanded.send(:slice, *slice)
20
+ end
21
+
22
+ expanded.transform_keys do |key|
23
+ "#{ key }_url"
24
+ end
25
+ end
26
+
27
+ def route_variables
28
+ serializer = self
29
+
30
+ self.class.route_variables.reduce({}) do |memo, var|
31
+ value = case
32
+ when serializer.respond_to?(var)
33
+ serializer.send(var)
34
+ when serializer.object.respond_to?(var)
35
+ serializer.object.send(var)
36
+ else
37
+ serializer.read_attribute_for_serialization(var)
38
+ end
39
+
40
+ memo[var] = value
41
+ memo
42
+ end
43
+ end
44
+
45
+ def self.route_variables
46
+ @resource_route_variables ||= parent_resource.router.route_patterns_table.map { |p| _, h = p; h[:variables] }.flatten.compact.uniq
47
+ end
48
+
49
+ def self.method_added(method_name)
12
50
  if documented = inline_description
13
51
  attribute_descriptions[method_name.to_sym] = documented
14
52
  end
@@ -22,7 +60,7 @@ module Smooth
22
60
  schema[:associations]
23
61
  end
24
62
 
25
- def self.configure options, resource=nil
63
+ def self.configure(options, resource = nil)
26
64
  resource ||= Smooth.current_resource
27
65
  klass = define_or_open(options, resource)
28
66
 
@@ -38,22 +76,53 @@ module Smooth
38
76
  base = Smooth.serializer
39
77
 
40
78
  name = options.name
41
- name = nil if name == "Default"
79
+ name = nil if name == 'Default'
80
+
81
+ klass = "#{ resource.model_class }#{ name }".singularize + 'Serializer'
42
82
 
43
- klass = "#{ resource_name }#{ name }".singularize + "Serializer"
83
+ klass = klass.gsub(/\s+/, '')
44
84
 
45
85
  if serializer_klass = Object.const_get(klass) rescue nil
46
86
  return serializer_klass
47
87
  end
48
88
 
49
- Object.const_set(klass, Class.new(base))
89
+ parent_klass = Class.new(base)
90
+
91
+ parent_klass.belongs_to_resource(resource)
92
+
93
+ begin
94
+ Object.const_set(klass, parent_klass)
95
+ rescue => ex
96
+ puts ex.message
97
+ puts "Error setting #{ klass } #{ base }. klass is a #{ klass.class }"
98
+ end
99
+
100
+ parent_klass
50
101
  end
51
102
 
52
- def self.documentation_for_attribute attribute
103
+ class_attribute :parent_resource
104
+
105
+ def parent_resource
106
+ self.class.parent_resource
107
+ end
108
+
109
+ def parent_api
110
+ self.class.parent_api
111
+ end
112
+
113
+ def self.belongs_to_resource(resource)
114
+ self.parent_resource = resource
115
+ end
116
+
117
+ def self.parent_api
118
+ parent_resource.api
119
+ end
120
+
121
+ def self.documentation_for_attribute(attribute)
53
122
  attribute_descriptions[attribute.to_sym]
54
123
  end
55
124
 
56
- def self.documentation_for_association association
125
+ def self.documentation_for_association(association)
57
126
  relationship_descriptions[association.to_sym]
58
127
  end
59
128
 
@@ -61,7 +130,11 @@ module Smooth
61
130
  attribute_descriptions.merge(relationship_descriptions).to_mash
62
131
  end
63
132
 
64
- def self.attribute attr, options={}
133
+ def self.interface_documentation
134
+ documentation
135
+ end
136
+
137
+ def self.attribute(attr, options = {})
65
138
  documented = inline_description
66
139
 
67
140
  if documented
@@ -71,7 +144,13 @@ module Smooth
71
144
  super
72
145
  end
73
146
 
74
- def self.has_one attr, options={}
147
+ def self.computed(*args, &block)
148
+ property_name = args.first
149
+ send(:define_method, property_name, &block)
150
+ send(:attribute, *args)
151
+ end
152
+
153
+ def self.has_one(attr, options = {})
75
154
  documented = inline_description
76
155
 
77
156
  if documented
@@ -81,7 +160,7 @@ module Smooth
81
160
  super
82
161
  end
83
162
 
84
- def self.has_many attr, options={}
163
+ def self.has_many(attr, options = {})
85
164
  documented = inline_description
86
165
 
87
166
  if documented
@@ -90,5 +169,17 @@ module Smooth
90
169
 
91
170
  super
92
171
  end
172
+
173
+ def self.return_ids_for_relationships!
174
+ @returns_ids_for_relationships = true
175
+ embed :ids
176
+ end
177
+
178
+ def self.returns_ids_for_relationships?
179
+ @returns_ids_for_relationships == true
180
+ end
181
+ end
182
+
183
+ class ArraySerializer < ActiveModel::ArraySerializer
93
184
  end
94
185
  end