spinebox 0.0.1

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 (44) hide show
  1. data/.gitignore +17 -0
  2. data/Gemfile +11 -0
  3. data/LICENSE +22 -0
  4. data/README.textile +26 -0
  5. data/Rakefile +2 -0
  6. data/bin/spinebox +32 -0
  7. data/lib/spinebox/base.rb +10 -0
  8. data/lib/spinebox/command.rb +53 -0
  9. data/lib/spinebox/compiler.rb +10 -0
  10. data/lib/spinebox/config.rb +22 -0
  11. data/lib/spinebox/generator.rb +24 -0
  12. data/lib/spinebox/routes.rb +41 -0
  13. data/lib/spinebox/templates/Gemfile +3 -0
  14. data/lib/spinebox/templates/Rakefile +0 -0
  15. data/lib/spinebox/templates/app/assets/images/.gitignore +0 -0
  16. data/lib/spinebox/templates/app/assets/javascripts/app/controllers/.gitignore +0 -0
  17. data/lib/spinebox/templates/app/assets/javascripts/app/index.js.coffee +6 -0
  18. data/lib/spinebox/templates/app/assets/javascripts/app/models/.gitignore +0 -0
  19. data/lib/spinebox/templates/app/assets/javascripts/app/views/.gitignore +0 -0
  20. data/lib/spinebox/templates/app/assets/javascripts/application.js +14 -0
  21. data/lib/spinebox/templates/app/assets/javascripts/lib/jquery.js +4 -0
  22. data/lib/spinebox/templates/app/assets/javascripts/lib/json2.js +485 -0
  23. data/lib/spinebox/templates/app/assets/javascripts/lib/spine/ajax.coffee +208 -0
  24. data/lib/spinebox/templates/app/assets/javascripts/lib/spine/list.coffee +43 -0
  25. data/lib/spinebox/templates/app/assets/javascripts/lib/spine/local.coffee +16 -0
  26. data/lib/spinebox/templates/app/assets/javascripts/lib/spine/manager.coffee +83 -0
  27. data/lib/spinebox/templates/app/assets/javascripts/lib/spine/relation.coffee +144 -0
  28. data/lib/spinebox/templates/app/assets/javascripts/lib/spine/route.coffee +145 -0
  29. data/lib/spinebox/templates/app/assets/javascripts/lib/spine/spine.coffee +537 -0
  30. data/lib/spinebox/templates/app/assets/stylesheets/application.css.scss +13 -0
  31. data/lib/spinebox/templates/config/config.rb +7 -0
  32. data/lib/spinebox/templates/config/routes.rb +13 -0
  33. data/lib/spinebox/templates/config.ru +3 -0
  34. data/lib/spinebox/templates/public/index.html +14 -0
  35. data/lib/spinebox/version.rb +3 -0
  36. data/lib/spinebox.rb +19 -0
  37. data/spec/base_spec.rb +9 -0
  38. data/spec/command_spec.rb +23 -0
  39. data/spec/config_spec.rb +29 -0
  40. data/spec/generator_spec.rb +25 -0
  41. data/spec/helpers.rb +9 -0
  42. data/spec/routes_spec.rb +14 -0
  43. data/spinebox.gemspec +17 -0
  44. metadata +97 -0
@@ -0,0 +1,537 @@
1
+ Events =
2
+ bind: (ev, callback) ->
3
+ evs = ev.split(' ')
4
+ calls = @hasOwnProperty('_callbacks') and @_callbacks or= {}
5
+
6
+ for name in evs
7
+ calls[name] or= []
8
+ calls[name].push(callback)
9
+ this
10
+
11
+ one: (ev, callback) ->
12
+ @bind ev, ->
13
+ @unbind(ev, arguments.callee)
14
+ callback.apply(@, arguments)
15
+
16
+ trigger: (args...) ->
17
+ ev = args.shift()
18
+
19
+ list = @hasOwnProperty('_callbacks') and @_callbacks?[ev]
20
+ return unless list
21
+
22
+ for callback in list
23
+ if callback.apply(@, args) is false
24
+ break
25
+ true
26
+
27
+ unbind: (ev, callback) ->
28
+ unless ev
29
+ @_callbacks = {}
30
+ return this
31
+
32
+ list = @_callbacks?[ev]
33
+ return this unless list
34
+
35
+ unless callback
36
+ delete @_callbacks[ev]
37
+ return this
38
+
39
+ for cb, i in list when cb is callback
40
+ list = list.slice()
41
+ list.splice(i, 1)
42
+ @_callbacks[ev] = list
43
+ break
44
+ this
45
+
46
+ Log =
47
+ trace: true
48
+
49
+ logPrefix: '(App)'
50
+
51
+ log: (args...) ->
52
+ return unless @trace
53
+ if @logPrefix then args.unshift(@logPrefix)
54
+ console?.log?(args...)
55
+ this
56
+
57
+ moduleKeywords = ['included', 'extended']
58
+
59
+ class Module
60
+ @include: (obj) ->
61
+ throw('include(obj) requires obj') unless obj
62
+ for key, value of obj when key not in moduleKeywords
63
+ @::[key] = value
64
+ obj.included?.apply(@)
65
+ this
66
+
67
+ @extend: (obj) ->
68
+ throw('extend(obj) requires obj') unless obj
69
+ for key, value of obj when key not in moduleKeywords
70
+ @[key] = value
71
+ obj.extended?.apply(@)
72
+ this
73
+
74
+ @proxy: (func) ->
75
+ => func.apply(@, arguments)
76
+
77
+ proxy: (func) ->
78
+ => func.apply(@, arguments)
79
+
80
+ constructor: ->
81
+ @init?(arguments...)
82
+
83
+ class Model extends Module
84
+ @extend Events
85
+
86
+ @records: {}
87
+ @crecords: {}
88
+ @attributes: []
89
+
90
+ @configure: (name, attributes...) ->
91
+ @className = name
92
+ @records = {}
93
+ @crecords = {}
94
+ @attributes = attributes if attributes.length
95
+ @attributes and= makeArray(@attributes)
96
+ @attributes or= []
97
+ @unbind()
98
+ this
99
+
100
+ @toString: -> "#{@className}(#{@attributes.join(", ")})"
101
+
102
+ @find: (id) ->
103
+ record = @records[id]
104
+ if !record and ("#{id}").match(/c-\d+/)
105
+ return @findCID(id)
106
+ throw('Unknown record') unless record
107
+ record.clone()
108
+
109
+ @findCID: (cid) ->
110
+ record = @crecords[cid]
111
+ throw('Unknown record') unless record
112
+ record.clone()
113
+
114
+ @exists: (id) ->
115
+ try
116
+ return @find(id)
117
+ catch e
118
+ return false
119
+
120
+ @refresh: (values, options = {}) ->
121
+ if options.clear
122
+ @records = {}
123
+ @crecords = {}
124
+
125
+ records = @fromJSON(values)
126
+ records = [records] unless isArray(records)
127
+
128
+ for record in records
129
+ record.id or= record.cid
130
+ @records[record.id] = record
131
+ @crecords[record.cid] = record
132
+
133
+ @resetIdCounter()
134
+
135
+ @trigger('refresh', @cloneArray(records))
136
+ this
137
+
138
+ @select: (callback) ->
139
+ result = (record for id, record of @records when callback(record))
140
+ @cloneArray(result)
141
+
142
+ @findByAttribute: (name, value) ->
143
+ for id, record of @records
144
+ if record[name] is value
145
+ return record.clone()
146
+ null
147
+
148
+ @findAllByAttribute: (name, value) ->
149
+ @select (item) ->
150
+ item[name] is value
151
+
152
+ @each: (callback) ->
153
+ for key, value of @records
154
+ callback(value.clone())
155
+
156
+ @all: ->
157
+ @cloneArray(@recordsValues())
158
+
159
+ @first: ->
160
+ record = @recordsValues()[0]
161
+ record?.clone()
162
+
163
+ @last: ->
164
+ values = @recordsValues()
165
+ record = values[values.length - 1]
166
+ record?.clone()
167
+
168
+ @count: ->
169
+ @recordsValues().length
170
+
171
+ @deleteAll: ->
172
+ for key, value of @records
173
+ delete @records[key]
174
+
175
+ @destroyAll: ->
176
+ for key, value of @records
177
+ @records[key].destroy()
178
+
179
+ @update: (id, atts, options) ->
180
+ @find(id).updateAttributes(atts, options)
181
+
182
+ @create: (atts, options) ->
183
+ record = new @(atts)
184
+ record.save(options)
185
+
186
+ @destroy: (id, options) ->
187
+ @find(id).destroy(options)
188
+
189
+ @change: (callbackOrParams) ->
190
+ if typeof callbackOrParams is 'function'
191
+ @bind('change', callbackOrParams)
192
+ else
193
+ @trigger('change', callbackOrParams)
194
+
195
+ @fetch: (callbackOrParams) ->
196
+ if typeof callbackOrParams is 'function'
197
+ @bind('fetch', callbackOrParams)
198
+ else
199
+ @trigger('fetch', callbackOrParams)
200
+
201
+ @toJSON: ->
202
+ @recordsValues()
203
+
204
+ @fromJSON: (objects) ->
205
+ return unless objects
206
+ if typeof objects is 'string'
207
+ objects = JSON.parse(objects)
208
+ if isArray(objects)
209
+ (new @(value) for value in objects)
210
+ else
211
+ new @(objects)
212
+
213
+ @fromForm: ->
214
+ (new this).fromForm(arguments...)
215
+
216
+ # Private
217
+
218
+ @recordsValues: ->
219
+ result = []
220
+ for key, value of @records
221
+ result.push(value)
222
+ result
223
+
224
+ @cloneArray: (array) ->
225
+ (value.clone() for value in array)
226
+
227
+ @idCounter: 0
228
+
229
+ @resetIdCounter: ->
230
+ ids = (model.id for model in @all()).sort((a, b) -> a > b)
231
+ lastID = ids[ids.length - 1]
232
+ lastID = lastID?.replace?(/^c-/, '') or lastID
233
+ lastID = parseInt(lastID, 10)
234
+ @idCounter = (lastID + 1) or 0
235
+
236
+ @uid: (prefix = '') ->
237
+ prefix + @idCounter++
238
+
239
+ # Instance
240
+
241
+ constructor: (atts) ->
242
+ super
243
+ @load atts if atts
244
+ @cid or= @constructor.uid('c-')
245
+
246
+ isNew: ->
247
+ not @exists()
248
+
249
+ isValid: ->
250
+ not @validate()
251
+
252
+ validate: ->
253
+
254
+ load: (atts) ->
255
+ for key, value of atts
256
+ if typeof @[key] is 'function'
257
+ @[key](value)
258
+ else
259
+ @[key] = value
260
+ this
261
+
262
+ attributes: ->
263
+ result = {}
264
+ for key in @constructor.attributes when key of this
265
+ if typeof @[key] is 'function'
266
+ result[key] = @[key]()
267
+ else
268
+ result[key] = @[key]
269
+ result.id = @id if @id
270
+ result
271
+
272
+ eql: (rec) ->
273
+ !!(rec and rec.constructor is @constructor and
274
+ (rec.cid is @cid) or (rec.id and rec.id is @id))
275
+
276
+ save: (options = {}) ->
277
+ unless options.validate is false
278
+ error = @validate()
279
+ if error
280
+ @trigger('error', error)
281
+ return false
282
+
283
+ @trigger('beforeSave', options)
284
+ record = if @isNew() then @create(options) else @update(options)
285
+ @trigger('save', options)
286
+ record
287
+
288
+ updateAttribute: (name, value, options) ->
289
+ @[name] = value
290
+ @save(options)
291
+
292
+ updateAttributes: (atts, options) ->
293
+ @load(atts)
294
+ @save(options)
295
+
296
+ changeID: (id) ->
297
+ records = @constructor.records
298
+ records[id] = records[@id]
299
+ delete records[@id]
300
+ @id = id
301
+ @save()
302
+
303
+ destroy: (options = {}) ->
304
+ @trigger('beforeDestroy', options)
305
+ delete @constructor.records[@id]
306
+ delete @constructor.crecords[@cid]
307
+ @destroyed = true
308
+ @trigger('destroy', options)
309
+ @trigger('change', 'destroy', options)
310
+ @unbind()
311
+ this
312
+
313
+ dup: (newRecord) ->
314
+ result = new @constructor(@attributes())
315
+ if newRecord is false
316
+ result.cid = @cid
317
+ else
318
+ delete result.id
319
+ result
320
+
321
+ clone: ->
322
+ createObject(@)
323
+
324
+ reload: ->
325
+ return this if @isNew()
326
+ original = @constructor.find(@id)
327
+ @load(original.attributes())
328
+ original
329
+
330
+ toJSON: ->
331
+ @attributes()
332
+
333
+ toString: ->
334
+ "<#{@constructor.className} (#{JSON.stringify(@)})>"
335
+
336
+ fromForm: (form) ->
337
+ result = {}
338
+ for key in $(form).serializeArray()
339
+ result[key.name] = key.value
340
+ @load(result)
341
+
342
+ exists: ->
343
+ @id && @id of @constructor.records
344
+
345
+ # Private
346
+
347
+ update: (options) ->
348
+ @trigger('beforeUpdate', options)
349
+ records = @constructor.records
350
+ records[@id].load @attributes()
351
+ clone = records[@id].clone()
352
+ clone.trigger('update', options)
353
+ clone.trigger('change', 'update', options)
354
+ clone
355
+
356
+ create: (options) ->
357
+ @trigger('beforeCreate', options)
358
+ @id = @cid unless @id
359
+
360
+ record = @dup(false)
361
+ @constructor.records[@id] = record
362
+ @constructor.crecords[@cid] = record
363
+
364
+ clone = record.clone()
365
+ clone.trigger('create', options)
366
+ clone.trigger('change', 'create', options)
367
+ clone
368
+
369
+ bind: (events, callback) ->
370
+ @constructor.bind events, binder = (record) =>
371
+ if record && @eql(record)
372
+ callback.apply(@, arguments)
373
+ @constructor.bind 'unbind', unbinder = (record) =>
374
+ if record && @eql(record)
375
+ @constructor.unbind(events, binder)
376
+ @constructor.unbind('unbind', unbinder)
377
+ binder
378
+
379
+ one: (events, callback) ->
380
+ binder = @bind events, =>
381
+ @constructor.unbind(events, binder)
382
+ callback.apply(@, arguments)
383
+
384
+ trigger: (args...) ->
385
+ args.splice(1, 0, @)
386
+ @constructor.trigger(args...)
387
+
388
+ unbind: ->
389
+ @trigger('unbind')
390
+
391
+ class Controller extends Module
392
+ @include Events
393
+ @include Log
394
+
395
+ eventSplitter: /^(\S+)\s*(.*)$/
396
+ tag: 'div'
397
+
398
+ constructor: (options) ->
399
+ @options = options
400
+
401
+ for key, value of @options
402
+ @[key] = value
403
+
404
+ @el = document.createElement(@tag) unless @el
405
+ @el = $(@el)
406
+
407
+ @el.addClass(@className) if @className
408
+ @el.attr(@attributes) if @attributes
409
+
410
+ @events = @constructor.events unless @events
411
+ @elements = @constructor.elements unless @elements
412
+
413
+ @delegateEvents(@events) if @events
414
+ @refreshElements() if @elements
415
+
416
+ super
417
+
418
+ release: =>
419
+ @el.remove()
420
+ @unbind()
421
+ @trigger 'release'
422
+
423
+ $: (selector) -> $(selector, @el)
424
+
425
+ delegateEvents: (events) ->
426
+ for key, method of events
427
+
428
+ unless typeof(method) is 'function'
429
+ # Always return true from event handlers
430
+ method = do (method) => =>
431
+ @[method].apply(this, arguments)
432
+ true
433
+
434
+ match = key.match(@eventSplitter)
435
+ eventName = match[1]
436
+ selector = match[2]
437
+
438
+ if selector is ''
439
+ @el.bind(eventName, method)
440
+ else
441
+ @el.delegate(selector, eventName, method)
442
+
443
+ refreshElements: ->
444
+ for key, value of @elements
445
+ @[value] = @$(key)
446
+
447
+ delay: (func, timeout) ->
448
+ setTimeout(@proxy(func), timeout || 0)
449
+
450
+ html: (element) ->
451
+ @el.html(element.el or element)
452
+ @refreshElements()
453
+ @el
454
+
455
+ append: (elements...) ->
456
+ elements = (e.el or e for e in elements)
457
+ @el.append(elements...)
458
+ @refreshElements()
459
+ @el
460
+
461
+ appendTo: (element) ->
462
+ @el.appendTo(element.el or element)
463
+ @refreshElements()
464
+ @el
465
+
466
+ prepend: (elements...) ->
467
+ elements = (e.el or e for e in elements)
468
+ @el.prepend(elements...)
469
+ @refreshElements()
470
+ @el
471
+
472
+ replace: (element) ->
473
+ [previous, @el] = [@el, $(element.el or element)]
474
+ previous.replaceWith(@el)
475
+ @delegateEvents(@events)
476
+ @refreshElements()
477
+ @el
478
+
479
+ # Utilities & Shims
480
+
481
+ $ = window?.jQuery or window?.Zepto or (element) -> element
482
+
483
+ createObject = Object.create or (o) ->
484
+ Func = ->
485
+ Func.prototype = o
486
+ new Func()
487
+
488
+ isArray = (value) ->
489
+ Object::toString.call(value) is '[object Array]'
490
+
491
+ isBlank = (value) ->
492
+ return true unless value
493
+ return false for key of value
494
+ true
495
+
496
+ makeArray = (args) ->
497
+ Array::slice.call(args, 0)
498
+
499
+ # Globals
500
+
501
+ Spine = @Spine = {}
502
+ module?.exports = Spine
503
+
504
+ Spine.version = '1.0.6'
505
+ Spine.isArray = isArray
506
+ Spine.isBlank = isBlank
507
+ Spine.$ = $
508
+ Spine.Events = Events
509
+ Spine.Log = Log
510
+ Spine.Module = Module
511
+ Spine.Controller = Controller
512
+ Spine.Model = Model
513
+
514
+ # Global events
515
+
516
+ Module.extend.call(Spine, Events)
517
+
518
+ # JavaScript compatability
519
+
520
+ Module.create = Module.sub =
521
+ Controller.create = Controller.sub =
522
+ Model.sub = (instances, statics) ->
523
+ class result extends this
524
+ result.include(instances) if instances
525
+ result.extend(statics) if statics
526
+ result.unbind?()
527
+ result
528
+
529
+ Model.setup = (name, attributes = []) ->
530
+ class Instance extends this
531
+ Instance.configure(name, attributes...)
532
+ Instance
533
+
534
+ Module.init = Controller.init = Model.init = (a1, a2, a3, a4, a5) ->
535
+ new this(a1, a2, a3, a4, a5)
536
+
537
+ Spine.Class = Module
@@ -0,0 +1,13 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the top of the
9
+ * compiled file, but it's generally better to create a new file per style scope.
10
+ *
11
+ *= require_self
12
+ *= require_tree .
13
+ */
@@ -0,0 +1,7 @@
1
+ Spinebox.config do |config|
2
+
3
+ # Setup Sprockets paths
4
+ config.assets.append_path 'app/assets/javascripts'
5
+ config.assets.append_path 'app/assets/stylesheets'
6
+
7
+ end
@@ -0,0 +1,13 @@
1
+ Spinebox::Routes.draw do
2
+
3
+ # Assets
4
+ map '/assets' do
5
+ run Spinebox.assets
6
+ end
7
+
8
+ # Root
9
+ map '/' do
10
+ run Rack::File.new "public/index.html"
11
+ end
12
+
13
+ end
@@ -0,0 +1,3 @@
1
+ require 'spinebox'
2
+
3
+ run Spinebox.app
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Spinebox</title>
5
+ <link href="/assets/application.css" media="all" rel="stylesheet" type="text/css">
6
+ <script src="/assets/application.js" type="text/javascript"></script>
7
+ </head>
8
+ <body>
9
+
10
+ <h1>Spinebox</h1>
11
+ <p>Is working!</p>
12
+
13
+ </body>
14
+ </html>
@@ -0,0 +1,3 @@
1
+ module Spinebox
2
+ VERSION = "0.0.1"
3
+ end
data/lib/spinebox.rb ADDED
@@ -0,0 +1,19 @@
1
+ require "sprockets"
2
+ require "rack"
3
+ require 'coffee_script'
4
+ require 'eco'
5
+ require 'sass'
6
+ require "ostruct"
7
+
8
+ require "spinebox/version"
9
+ require "spinebox/base"
10
+ require "spinebox/command"
11
+ require "spinebox/config"
12
+ require "spinebox/routes"
13
+ require "spinebox/generator"
14
+
15
+ module Spinebox
16
+ class << self
17
+ include Spinebox::Base
18
+ end
19
+ end
data/spec/base_spec.rb ADDED
@@ -0,0 +1,9 @@
1
+ require_relative "./helpers"
2
+
3
+ describe Spinebox::Base do
4
+
5
+ it "should return the root url of the gem" do
6
+ File.exists?("#{Spinebox.root}/version.rb").should be_true
7
+ end
8
+
9
+ end
@@ -0,0 +1,23 @@
1
+ require_relative "./helpers"
2
+
3
+ describe Spinebox::Command do
4
+
5
+ it "should store commands" do
6
+ Spinebox::Command.dispatch do
7
+ on "--test, -t", "A test case", :if => proc { true } do
8
+
9
+ end
10
+
11
+ on "--test2, -t2", "A test case 2", :if => proc { true } do
12
+
13
+ end
14
+ end
15
+
16
+ Spinebox::Command.commands.should have(2).command
17
+ end
18
+
19
+ it "should execute commands" do
20
+ Spinebox::Command.execute
21
+ end
22
+
23
+ end
@@ -0,0 +1,29 @@
1
+ require_relative "./helpers"
2
+
3
+ describe Spinebox do
4
+
5
+ it "should offer a block style config method" do
6
+ Spinebox.config do |config|
7
+ config.should be_a OpenStruct
8
+ config.assets.should be_a Sprockets::Environment
9
+ end
10
+ end
11
+
12
+ it "should offer a normal configuration" do
13
+ Spinebox.config.should be_a OpenStruct
14
+ Spinebox.config.assets.should be_a Sprockets::Environment
15
+ end
16
+
17
+ it "should offer straight access to the assets" do
18
+ Spinebox.assets.should be_a Sprockets::Environment
19
+ end
20
+
21
+ it "should load the config from the default path" do
22
+ Dir.chdir "#{Spinebox.root}/templates"
23
+
24
+ Spinebox.config.assets.paths.should have(0).paths
25
+ Spinebox.load_config!
26
+ Spinebox.config.assets.paths.should have_at_least(2).paths
27
+ end
28
+
29
+ end
@@ -0,0 +1,25 @@
1
+ require_relative "./helpers"
2
+
3
+ describe Spinebox::Generator do
4
+
5
+ before(:each) do
6
+ Dir.chdir(File.dirname(__FILE__))
7
+ FileUtils.rm_rf("tmp")
8
+ FileUtils.mkdir("tmp")
9
+ Dir.chdir "tmp"
10
+ end
11
+
12
+ after(:each) do
13
+ Dir.chdir ".."
14
+ FileUtils.rm_rf("tmp")
15
+ end
16
+
17
+ it "should generate app with name" do
18
+ File.directory?("box").should_not be_true
19
+
20
+ generator = Spinebox::Generator.new "box"
21
+ File.directory?(generator.root).should be_true
22
+ Dir["#{generator.root}/**/*"].should have_at_least(5).files
23
+ end
24
+
25
+ end