brainstem 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in brainstem.gemspec
4
+ gemspec
@@ -0,0 +1,51 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ brainstem (0.0.2)
5
+ activerecord (~> 3.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activemodel (3.2.13)
11
+ activesupport (= 3.2.13)
12
+ builder (~> 3.0.0)
13
+ activerecord (3.2.13)
14
+ activemodel (= 3.2.13)
15
+ activesupport (= 3.2.13)
16
+ arel (~> 3.0.2)
17
+ tzinfo (~> 0.3.29)
18
+ activesupport (3.2.13)
19
+ i18n (= 0.6.1)
20
+ multi_json (~> 1.0)
21
+ arel (3.0.2)
22
+ builder (3.0.4)
23
+ diff-lcs (1.2.2)
24
+ i18n (0.6.1)
25
+ multi_json (1.7.2)
26
+ rake (10.0.4)
27
+ redcarpet (2.2.2)
28
+ rr (1.0.5)
29
+ rspec (2.13.0)
30
+ rspec-core (~> 2.13.0)
31
+ rspec-expectations (~> 2.13.0)
32
+ rspec-mocks (~> 2.13.0)
33
+ rspec-core (2.13.1)
34
+ rspec-expectations (2.13.0)
35
+ diff-lcs (>= 1.1.3, < 2.0)
36
+ rspec-mocks (2.13.1)
37
+ sqlite3 (1.3.7)
38
+ tzinfo (0.3.37)
39
+ yard (0.8.5.2)
40
+
41
+ PLATFORMS
42
+ ruby
43
+
44
+ DEPENDENCIES
45
+ brainstem!
46
+ rake
47
+ redcarpet
48
+ rr
49
+ rspec
50
+ sqlite3
51
+ yard
@@ -0,0 +1,8 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'rspec' do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+ end
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Mavenlink, Inc.
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,181 @@
1
+ # Brainstem
2
+
3
+ Brainstem is designed to power rich APIs in Rails. The Brainstem gem provides a presenter library that handles converting ActiveRecord objects into structured JSON and a set of API abstractions that allow users to request sorts, filters, and association loads, allowing for simpler implementations, fewer requests, and smaller responses.
4
+
5
+ ## Why Brainstem?
6
+
7
+ * Seperate business and presentation logic with Presenters.
8
+ * Version your Presenters for consistency as your API evolves.
9
+ * Expose end-user selectable filters and sorts.
10
+ * Whitelist your existing scopes to act as API filters for your users.
11
+ * Allow users to side-load multiple objects, with their associations, in a single request, reducing the number of requests needed to get the job done. This is especially helpful for building speedy mobile applications.
12
+ * Prevent data duplication by pulling associations into top-level hashes, easily indexable by ID.
13
+ * Easy integration with Backbone.js. "It's like Ember Data for Backbone.js!"
14
+
15
+ ## Installation
16
+
17
+ Add this line to your application's Gemfile:
18
+
19
+ gem 'brainstem'
20
+
21
+ ## Usage
22
+
23
+ Create a class that inherits from Brainstem::Presenter, named after the model that you want to present, and preferrably versioned in a module. For example:
24
+
25
+ module Api
26
+ module V1
27
+ class WidgetPresenter < Brainstem::Presenter
28
+ presents "Widget"
29
+
30
+ # Available sort orders to expose through the API
31
+ sort_order :updated_at, "widgets.updated_at"
32
+ sort_order :created_at, "widgets.created_at"
33
+
34
+ # Default sort order to apply
35
+ default_sort_order "updated_at:desc"
36
+
37
+ # Optional filter that delegates to the Widget model :popular scope,
38
+ # which should take one argument of true or false.
39
+ filter :popular
40
+
41
+ # Optional filter that applies a lambda.
42
+ filter :location_name do |scope, location_name|
43
+ scope.joins(:locations).where("locations.name = ?", location_name)
44
+ end
45
+
46
+ # Filter with an overridable default that runs on all requests.
47
+ filter :include_legacy_widgets, :default => false do |scope, bool|
48
+ bool ? scope : scope.without_legacy_widgets
49
+ end
50
+
51
+ # Return a ruby hash that can be converted to JSON
52
+ def present(widget)
53
+ {
54
+ :name => widget.name,
55
+ :legacy => widget.legacy?,
56
+ :updated_at => widget.updated_at,
57
+ :created_at => widget.created_at,
58
+ # Associations can be included by request
59
+ :features => association(:features),
60
+ :location => association(:location)
61
+ }
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ Once you've created a presenter like the one above, pass requests through from your controller.
68
+
69
+ class Api::WidgetsController < ActionController::Base
70
+ include Brainstem::ControllerMethods
71
+
72
+ def index
73
+ render :json => present("widgets") { Widgets.visible_to(current_user) }
74
+ end
75
+ end
76
+
77
+ The scope passed to `present` could contain any starting conditions that you'd like. Requests can have includes, filters, and sort orders.
78
+
79
+ GET /api/widgets.json?include=features&sort_order=popularity:desc&location_name=san+francisco
80
+
81
+ Responses will look like the following:
82
+
83
+ {
84
+ # Total number of results that matched the query.
85
+ count: 5,
86
+
87
+ # A lookup table to top-level keys. Necessary
88
+ # because some objects can have associations of
89
+ # the same type as themselves.
90
+ results: [
91
+ { key: "widgets", id: "2" },
92
+ { key: "widgets", id: "10" }
93
+ ],
94
+
95
+ # Serialized models with any requested associations, keyed by ID.
96
+
97
+ widgets: {
98
+ "10": {
99
+ id: "10",
100
+ name: "disco ball",
101
+ feature_ids: ["5"],
102
+ popularity: 85,
103
+ location_id: "2"
104
+ },
105
+
106
+ "2": {
107
+ id: "2",
108
+ name: "flubber",
109
+ feature_ids: ["6", "12"],
110
+ popularity: 100,
111
+ location_id: "2"
112
+ }
113
+ },
114
+
115
+ features: {
116
+ "5": { id: "5", name: "shiny" },
117
+ "6": { id: "6", name: "bouncy" },
118
+ "12": { id: "12", name: "physically impossible" }
119
+ }
120
+ }
121
+
122
+ You may want to setup an initializer in `config/initializers/brainstem.rb` like the following:
123
+
124
+ Brainstem.default_namespace = :v1
125
+
126
+ module Api
127
+ module V1
128
+ module Helper
129
+ def current_user
130
+ # However you get your current user.
131
+ end
132
+ end
133
+ end
134
+ end
135
+ Brainstem::Presenter.helper(Api::V1::Helper)
136
+
137
+ require 'api/v1/widget_presenter'
138
+ require 'api/v1/feature_presenter'
139
+ require 'api/v1/location_presenter'
140
+ # ...
141
+
142
+ # Or you could do something like this:
143
+ # Dir[Rails.root.join("lib/api/v1/*_presenter.rb").to_s].each { |p| require p }
144
+
145
+ For more detailed examples, please see the documentation for methods on {Brainstem::Presenter} and our detailed [Rails example application](https://github.com/mavenlink/brainstem-demo-rails).
146
+
147
+ ## Consuming a Brainstem API
148
+
149
+ APIs presented with Brainstem are just JSON APIs, so they can be consumed with just about any language. As Brainstem evolves, we hope that people will contributed consumption libraries in various languages.
150
+
151
+ ### The Results Array
152
+
153
+ {
154
+ results: [
155
+ { key: "widgets", id: "2" }, { key: "widgets", id: "10" }
156
+ ],
157
+
158
+ widgets: {
159
+ "10": {
160
+ id: "10",
161
+ name: "disco ball",
162
+
163
+
164
+
165
+ Brainstem returns objects as top-level hashes and provides a `results` array of `key` and `id` objects for finding the returned data in those hashes. The reason that we use the `results` array is two-fold: 1st) it provides order outside of the serialized objects so that we can provide objects keyed by ID, and 2nd) it allows for polymorphic responses and for objects that have associations of their own type (like posts and replies or tasks and sub-tasks).
166
+
167
+ ### Brainstem and Backbone.js
168
+
169
+ If you're already using Backbone.js, integrating with a Brainstem API is super simple. Just use the [Brainstem.js](https://github.com/mavenlink/brainstem-js) gem (or its JavaScript contents) to access your relational Brainstem API from JavaScript.
170
+
171
+ ## Contributing
172
+
173
+ 1. Fork Brainstem or Brainstem.js
174
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
175
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
176
+ 4. Push to the branch (`git push origin my-new-feature`)
177
+ 5. Create new Pull Request (`git pull-request`)
178
+
179
+ ## License
180
+
181
+ Brainstem and Brainstem.js were created by Mavenlink, Inc. and are available under the MIT License.
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ require "rspec/core/rake_task"
5
+ RSpec::Core::RakeTask.new(:spec)
6
+ task :default => :spec
@@ -0,0 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
3
+ require "brainstem/version"
4
+
5
+ Gem::Specification.new do |gem|
6
+ gem.name = "brainstem"
7
+ gem.authors = ["Sufyan Adam", "André Arko", "Andrew Cantino", "Katlyn Daniluk", "Reid Gillette"]
8
+ gem.email = ["dev@mavenlink.com"]
9
+ gem.description = %q{Brainstem allows you to create rich API presenters that know how to filter, sort, and include associations.}
10
+ gem.summary = %q{ActiveRecord presenters with a rich request API}
11
+ gem.homepage = "http://developer.mavenlink.com"
12
+ gem.license = "MIT"
13
+
14
+ gem.files = Dir["**/*"]
15
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
16
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
17
+ gem.require_paths = ["lib"]
18
+ gem.version = Brainstem::VERSION
19
+
20
+ gem.add_dependency "activerecord", "~> 3.0"
21
+
22
+ gem.add_development_dependency "rake"
23
+ gem.add_development_dependency "redcarpet" # for markdown in yard
24
+ gem.add_development_dependency "rr"
25
+ gem.add_development_dependency "rspec"
26
+ gem.add_development_dependency "sqlite3"
27
+ gem.add_development_dependency "yard"
28
+ end
@@ -0,0 +1,63 @@
1
+ require "brainstem/version"
2
+ require "brainstem/presenter"
3
+ require "brainstem/presenter_collection"
4
+ require "brainstem/controller_methods"
5
+
6
+ # The Brainstem module itself contains a +default_namespace+ class attribute and a few helpers that make managing +PresenterCollections+ and their corresponding namespaces easier.
7
+ module Brainstem
8
+
9
+ # Sets {default_namespace} to a new value.
10
+ # @param [String] namespace
11
+ # @return [String] the new default namespace
12
+ def self.default_namespace=(namespace)
13
+ @default_namespace = namespace
14
+ end
15
+
16
+ # The namespace that will be used by {presenter_collection} and {add_presenter_class} if none is given or implied.
17
+ # @return [String] the default namespace
18
+ def self.default_namespace
19
+ @default_namespace || "none"
20
+ end
21
+
22
+ # @param [String] namespace
23
+ # @return [PresenterCollection] the {PresenterCollection} for the given namespace.
24
+ def self.presenter_collection(namespace = nil)
25
+ namespace ||= default_namespace
26
+ @presenter_collection ||= {}
27
+ @presenter_collection[namespace.to_s.downcase] ||= PresenterCollection.new
28
+ end
29
+
30
+ # Helper method to quickly add presenter classes that are in a namespace. For example, +add_presenter_class(Api::V1::UserPresenter, "User")+ would add +UserPresenter+ to the PresenterCollection for the +:v1+ namespace as the presenter for the +User+ class.
31
+ # @param [Brainstem::Presenter] presenter_class The presenter class that is being registered.
32
+ # @param [Array<String, Class>] klasses Classes that will be presented by the given presenter.
33
+ def self.add_presenter_class(presenter_class, *klasses)
34
+ presenter_collection(namespace_of(presenter_class)).add_presenter_class(presenter_class, *klasses)
35
+ end
36
+
37
+ # @param [Class] klass The Ruby class whose namespace we would like to know.
38
+ # @return [String] The name of the module containing the passed-in class.
39
+ def self.namespace_of(klass)
40
+ names = klass.to_s.split("::")
41
+ names[-2] ? names[-2] : default_namespace
42
+ end
43
+
44
+ # @return [Logger] The Brainstem logger. If Rails is loaded, defaults to the Rails logger. If Rails is not loaded, defaults to a STDOUT logger.
45
+ def self.logger
46
+ @logger ||= begin
47
+ if defined?(Rails)
48
+ Rails.logger
49
+ else
50
+ require "logger"
51
+ Logger.new(STDOUT)
52
+ end
53
+ end
54
+ end
55
+
56
+ # Sets a new Brainstem logger.
57
+ # @param [Logger] logger A new Brainstem logger.
58
+ # @return [Logger] The new Brainstem logger.
59
+ def self.logger=(logger)
60
+ @logger = logger
61
+ end
62
+
63
+ end
@@ -0,0 +1,35 @@
1
+ module Brainstem
2
+ # AssociationField acts as a standin for associations.
3
+ # @api private
4
+ class AssociationField
5
+ # @!attribute [r] method_name
6
+ # @return [Symbol] The name of the method that is being proxied.
7
+ attr_reader :method_name
8
+
9
+ # @!attribute [r] json_name
10
+ # @return [Symbol] The name of the top-level JSON key for objects provided by this association.
11
+ attr_accessor :json_name
12
+
13
+ # @param method_name The name of the method being proxied. Not required if
14
+ # a block is passed instead.
15
+ # @option options [Boolean] :json_name The name of the top-level JSON key for objects provided by this association.
16
+ def initialize(method_name = nil, options = {}, &block)
17
+ @json_name = options[:json_name]
18
+ if block_given?
19
+ raise ArgumentError, "Method name is invalid with a block" if method_name
20
+ @block = block
21
+ elsif method_name
22
+ @method_name = method_name
23
+ else
24
+ raise ArgumentError, "Method name or block is required"
25
+ end
26
+ end
27
+
28
+ # Call the method or block being proxied.
29
+ # @param model The object to call the proxied method on.
30
+ # @return The value returned by calling the method or block being proxied.
31
+ def call(model)
32
+ @block ? @block.call : model.send(@method_name)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,44 @@
1
+ module Brainstem
2
+
3
+ # ControllerMethods are intended to be included into controllers that will be handling requests for presented objects.
4
+ # The present method will pass through +params+, so that any allowed and requested includes, filters, sort orders
5
+ # will be applied to the presented data.
6
+ module ControllerMethods
7
+
8
+ # Return a Ruby hash that contains models requested by the user's params and allowed
9
+ # by the +name+ presenter's configuration.
10
+ #
11
+ # Pass the returned hash to the render method to convert it into a useful format.
12
+ # For example:
13
+ # render :json => present("post"){ Post.where(:draft => false) }
14
+ # @param (see PresenterCollection#presenting)
15
+ # @option options [String] :namespace ("none") the namespace to be presented from
16
+ # @yield (see PresenterCollection#presenting)
17
+ # @return (see PresenterCollection#presenting)
18
+ def present(name, options = {}, &block)
19
+ Brainstem.presenter_collection(options[:namespace]).presenting(name, options.reverse_merge(:params => params), &block)
20
+ end
21
+
22
+ # Similar to ControllerMethods#present, but always returns all of the given objects, not just those that match any provided
23
+ # filters.
24
+ # @option options [String] :namespace ("none") the namespace to be presented from
25
+ # @option options [Hash] :key_map a Hash from Class name to json key name, if desired.
26
+ # e.g., map 'SystemWidgets' objects to the 'widgets' key in the JSON. This is
27
+ # only required if the name cannot be inferred.
28
+ # @return (see PresenterCollection#presenting)
29
+ def present_object(objects, options = {})
30
+ options.reverse_merge(:params => params)
31
+ if objects.is_a?(ActiveRecord::Relation) || objects.is_a?(Array)
32
+ raise ActiveRecord::RecordNotFound if objects.empty?
33
+ klass = objects.first.class
34
+ ids = objects.map(&:id)
35
+ else
36
+ klass = objects.class
37
+ ids = objects.id
38
+ end
39
+ json_key = (options[:key_map] || {})[klass.to_s] || klass.table_name
40
+ present(klass, :as => json_key, :apply_default_filters => false){ klass.where(:id => ids) }
41
+ end
42
+ alias_method :present_objects, :present_object
43
+ end
44
+ end