api-twister 0.0.1

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 "http://rubygems.org"
2
+
3
+ # see api-twister.gemspec
4
+ gemspec
@@ -0,0 +1,27 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ api-twister (0.0.1)
5
+ activesupport (>= 2.1.0)
6
+
7
+ GEM
8
+ remote: http://rubygems.org/
9
+ specs:
10
+ activesupport (3.0.3)
11
+ i18n (0.5.0)
12
+ mocha (0.9.10)
13
+ rake
14
+ rake (0.8.7)
15
+ shoulda (2.11.3)
16
+ test-unit (2.1.2)
17
+
18
+ PLATFORMS
19
+ ruby
20
+
21
+ DEPENDENCIES
22
+ activesupport (>= 2.1.0)
23
+ api-twister!
24
+ i18n
25
+ mocha
26
+ shoulda
27
+ test-unit
@@ -0,0 +1,91 @@
1
+ = API Twister Ruby Gem
2
+
3
+ == What
4
+
5
+ The api-twister gem is a ruby DSL to define non-trivial APIs.
6
+
7
+ It works with Rails 2, Rails 3, and without Rails (you will need ActiveSupport, though). It works with ruby 1.9.1 and 1.9.2, and possibly others.
8
+
9
+ == Why
10
+
11
+ Defining a simple REST API in Rails that returns simple objects is easy. Do it.
12
+
13
+ However, if you are returning nested objects, want to include methods in your responses, or want to exclude certain attributes, the basic configuration methods can get ugly quickly. Also if you have different serialization options and do not want to use HTTP response codes to describe them (for example, no results were found in a search), things can get ugly. This library hides much of the ugly. It puts knowledge of what attributes, methods, and associations to return in each model class. It allows you to define different named serialization options.
14
+
15
+ == Installation
16
+
17
+ gem install api-twister
18
+
19
+ === Rails 3:
20
+
21
+ Add to your Gemfile:
22
+
23
+ gem 'api-twister'
24
+
25
+ === Rails 2:
26
+
27
+ Add to your environment.rb:
28
+
29
+ config.gem 'api-twister'
30
+
31
+ == Development
32
+
33
+ Fork away. Please create a topic branch and write passing tests if you are submitting a pull request.
34
+
35
+ git clone git://github.com/yourname/api-twister.git
36
+ cd api-twister
37
+ bundle install
38
+ rake test
39
+ git checkout -b your_fix
40
+
41
+ == Show me some code!
42
+
43
+ class Monkey < ActiveRecord::Base
44
+ has_many :bananas
45
+
46
+ include ApiTwister
47
+
48
+ define_api do |api|
49
+ api.methods :behavior, :favorite_number, :status
50
+ api.association :bananas
51
+ end
52
+
53
+ api :request, :only => [:name]
54
+ api :response # default is everything in define_api
55
+ api :error_response, :only => [:status]
56
+
57
+ def behavior
58
+ name == 'Mr. Bananas' ? 'Good' : 'Bad'
59
+ end
60
+
61
+ def favorite_number
62
+ rand(10)
63
+ end
64
+
65
+ def status
66
+ valid? ? 'OK' : 'Error'
67
+ end
68
+ end
69
+
70
+ class Banana < ActiveRecord::Base
71
+ belongs_to :monkey
72
+
73
+ include ApiTwister
74
+
75
+ define_api {|a| a.method :edible}
76
+
77
+ api :request
78
+ api :response
79
+
80
+ def edible
81
+ created_at > 10.days.ago ? 'Yes' : 'No'
82
+ end
83
+ end
84
+
85
+ # TODO - more docs
86
+
87
+ == Credits
88
+
89
+ Scott Jacobsen
90
+
91
+ Tee Parham
@@ -0,0 +1,11 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ require 'bundler'
5
+ Bundler::GemHelper.install_tasks
6
+
7
+ require 'rake/testtask'
8
+ Rake::TestTask.new(:test) do |t|
9
+ t.test_files = ['test/**/*.rb']
10
+ t.verbose = false
11
+ end
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "api-twister/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "api-twister"
7
+ s.version = Api::Twister::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Scott Jacobsen", "Tee Parham"]
10
+ s.email = ["hello@stackpilot.com"]
11
+ s.homepage = ""
12
+ s.summary = %q{Ruby DSL for non-trivial API specifications}
13
+ s.description = %q{Ruby DSL for non-trivial API specifications}
14
+
15
+ s.rubyforge_project = "api-twister"
16
+
17
+ s.files = `git ls-files`.split("\n").delete_if {|file| %w(.rvmrc .gitignore).include? file}
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ s.add_dependency "activesupport", ">= 2.1.0" # for class_inheritable_accessor
23
+ s.add_development_dependency "i18n" #appears to be required by active support
24
+ s.add_development_dependency "test-unit"
25
+ s.add_development_dependency "shoulda"
26
+ s.add_development_dependency "mocha"
27
+ end
@@ -0,0 +1,11 @@
1
+ require 'active_support/core_ext/class/inheritable_attributes'
2
+ require 'active_support/core_ext/hash/keys'
3
+ require 'active_support/core_ext/string/inflections'
4
+
5
+ require 'api-twister/api_association'
6
+ require 'api-twister/api_attribute'
7
+ require 'api-twister/api_method'
8
+ require "api-twister/code_table"
9
+ require 'api-twister/api_definition'
10
+ require 'api-twister/api_twister'
11
+ require "api-twister/documentation"
@@ -0,0 +1,22 @@
1
+ module ApiTwister
2
+ class ApiAssociation
3
+ attr_reader :association
4
+
5
+ def initialize(model, association)
6
+ @model = model
7
+ @association = association
8
+ end
9
+
10
+ def api_hash(name, options={})
11
+ hash = {}
12
+ assoc = @model.reflect_on_association(@association)
13
+ raise "There is no association named #{@association}" if assoc.nil?
14
+ klass = eval(assoc.class_name)
15
+ if klass.respond_to? :api_hash
16
+ hash[@association] = klass.api_hash(name, options)
17
+ else
18
+ hash[@association] = {}
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,12 @@
1
+ module ApiTwister
2
+ class ApiAttribute
3
+ attr_accessor :name, :description, :data_type, :required
4
+
5
+ def initialize(name, data_type, description, required)
6
+ @name = name
7
+ @description = description
8
+ @data_type = data_type
9
+ @required = required
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,66 @@
1
+ module ApiTwister
2
+ class ApiDefinition
3
+ attr_reader :api_items
4
+ attr_accessor :model, :node_name
5
+
6
+ TYPE_MAP = {:association => ApiAssociation, :method => ApiMethod, :attribute => ApiAttribute, :code_table => CodeTable}
7
+ # Returns a list of symbols where the symbol is the name of the item
8
+ def all_items(type = nil)
9
+ if type
10
+ @api_items.select {|k, v| v.kind_of?(TYPE_MAP[type])}.keys
11
+ else
12
+ @api_items.keys
13
+ end
14
+ end
15
+
16
+ def all_objects(type = nil)
17
+ if type
18
+ @api_items.select {|k, v| v.kind_of?(TYPE_MAP[type])}.values
19
+ else
20
+ @api_items.values
21
+ end
22
+ end
23
+
24
+ def initialize(model, options = {})
25
+ @has_attributes = false
26
+ @model = model
27
+ @api_items = {}
28
+ @node_name = options[:node_name] || model.name.underscore.dasherize
29
+ end
30
+
31
+ def association(assoc)
32
+ @api_items[assoc] = ApiAssociation.new(model, assoc)
33
+ end
34
+
35
+ def associations(*assocs)
36
+ assocs.each {|a| association a}
37
+ end
38
+
39
+ def attribute(attrib, data_type = "string", description = nil, required = false)
40
+ @has_attributes = true
41
+ @api_items[attrib] = ApiAttribute.new(attrib, data_type, description, required)
42
+ end
43
+
44
+ def attributes(*attribs)
45
+ attribs.each {|a| attribute a }
46
+ end
47
+
48
+ alias_method :orig_method, :method
49
+ def method(meth, data_type = "string", description = nil, required = false)
50
+ @api_items[meth] = ApiMethod.new(meth, data_type, description, required)
51
+ end
52
+
53
+ alias_method :orig_methods, :methods
54
+ def methods(*meths)
55
+ meths.each {|m| method m}
56
+ end
57
+
58
+ def code_table(name, table_name, order_by=nil)
59
+ @api_items[name] = CodeTable.new(name, table_name, order_by)
60
+ end
61
+
62
+ def has_attributes?
63
+ @has_attributes
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,7 @@
1
+ module ApiTwister
2
+ class ApiDocumentation
3
+ def self.nodes_for_user(user)
4
+ root_api_objects = [Profile, Search]
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,4 @@
1
+ module ApiTwister
2
+ class ApiMethod < ApiAttribute
3
+ end
4
+ end
@@ -0,0 +1,114 @@
1
+ module ApiTwister
2
+ def ApiTwister.included(base)
3
+ base.class_eval <<-eos
4
+ class_inheritable_accessor :_api_definition
5
+ class_inheritable_accessor :_api_specifications
6
+ extend ClassMethods
7
+ include InstanceMethods
8
+ eos
9
+ end
10
+
11
+ module ClassMethods
12
+ def define_api(options={})
13
+ self._api_definition ||= ApiDefinition.new(self, options)
14
+
15
+ yield self._api_definition if block_given?
16
+
17
+ # if no attributes were added in block, add all the attributes here
18
+ unless self._api_definition.has_attributes?
19
+ self.new.attributes.symbolize_keys.keys.each { |k| _api_definition.attribute k }
20
+ end
21
+
22
+ self._api_definition
23
+ end
24
+
25
+ def api(name, options={})
26
+ self._api_specifications ||= {}
27
+ _api_specifications[name] ||= []
28
+
29
+ if options[:only]
30
+ options[:only].each do |only|
31
+ if [:methods, :attributes, :associations].include?(only)
32
+ _api_specifications[name] << _api_definition.all_items(only.to_s.singularize.to_sym)
33
+ else
34
+ _api_specifications[name] << only
35
+ end
36
+ end
37
+ elsif options[:except]
38
+ everything = _api_definition.all_items
39
+ except_list = []
40
+ options[:except].each do |except|
41
+ if [:methods, :attributes, :associations].include?(except)
42
+ except_list << _api_definition.all_items(except.to_s.singularize.to_sym)
43
+ else
44
+ except_list << except
45
+ end
46
+ end
47
+ except_list.flatten!
48
+ everything.flatten!
49
+ _api_specifications[name] = everything - except_list
50
+ else
51
+ self._api_specifications[name] = _api_definition.all_items
52
+ end
53
+ _api_specifications[name].flatten!
54
+ end
55
+
56
+ def api_hash(name, options = {})
57
+ spec = self._api_specifications[name]
58
+ hash = { :only => [] }
59
+ hash[:skip_types] = options[:skip_types] if options[:skip_types]
60
+ hash[:root] = options[:root] if options[:root]
61
+ user = options[:user]
62
+
63
+ spec.each do |item|
64
+ object = self._api_definition.api_items[item]
65
+ if object.is_a?(ApiMethod)
66
+ hash[:methods] ||= []
67
+ hash[:methods] << item if user_has_permission?(user, item)
68
+ elsif object.is_a?(ApiAttribute)
69
+ hash[:only] << item if user_has_permission?(user, item)
70
+ else
71
+ raise "Nil object. Spec: #{spec}, Item: #{item}" if object.nil?
72
+ hash[:include] ||= {}
73
+
74
+ hash[:include][item] = object.api_hash(name, options) if user_has_permission?(user, object.association)
75
+ end
76
+ end if spec
77
+ hash
78
+ end
79
+
80
+ def api_models(user)
81
+ #TODO: Test
82
+ models = [self] if user_has_permission?(user, self.name)
83
+ self._api_definition.all_objects(:association).each do |assoc|
84
+ models += assoc.model.api_models if user_has_permission?(user, assoc.association)
85
+ end
86
+ models
87
+ end
88
+
89
+ def exposes_as(item)
90
+ value = _api_definition.api_items[item.to_sym]
91
+ value.is_a?(ApiAssociation) ? :association : value
92
+ end
93
+
94
+ private
95
+
96
+ def user_has_permission?(user, item)
97
+ user.nil? || user.has_permission?(self, item)
98
+ end
99
+ end
100
+
101
+ module InstanceMethods
102
+ def api_hash(name, options={})
103
+ self.class.api_hash name, options
104
+ end
105
+
106
+ def to_api_xml(name, options={})
107
+ to_xml(api_hash(name, options))
108
+ end
109
+
110
+ def to_api_json(name, options={})
111
+ to_json(api_hash(name, options))
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,24 @@
1
+ module ApiTwister
2
+ class CodeTable
3
+ attr_accessor :name, :table_name
4
+
5
+ def initialize(name, table_name, order_by=nil)
6
+ @name = name
7
+ @table_name = table_name
8
+ @order_by = order_by
9
+ end
10
+
11
+ def code_values
12
+ @code_values ||= load_code_values
13
+ end
14
+
15
+ private
16
+
17
+ def load_code_values
18
+ sql = "select * from #{table_name}"
19
+ sql << " order by #{@order_by}" if @order_by
20
+ ActiveRecord::Base.connection.select_all(sql).collect{|r| CodeValue.new(r)}
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,30 @@
1
+ module ApiTwister
2
+ module Documentation
3
+ def Documentation.included(base)
4
+ base.class_eval <<-eos
5
+ class_inheritable_accessor :_api_definition
6
+ class_inheritable_accessor :_api_specifications
7
+ extend ClassMethods
8
+ include InstanceMethods
9
+ eos
10
+ end
11
+
12
+ module ClassMethods
13
+ def node_name(options = {})
14
+ #TODO: If a user is passed filter by permissions
15
+ self._api_definition.node_name
16
+ end
17
+
18
+ def doc_attributes(options = {})
19
+ # return a list of attributes and methods
20
+ #TODO: If a user is passed filter by permissions
21
+ self._api_definition.all_objects(:attribute) + self._api_definition.all_objects(:method)
22
+ end
23
+
24
+ def doc_code_tables(options = {})
25
+ #TODO: If a user is passed filter by permissions
26
+ self._api_definition.all_objects(:code_table)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,5 @@
1
+ module Api
2
+ module Twister
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,111 @@
1
+ require File.expand_path('test_helper.rb', File.dirname(__FILE__))
2
+ require 'mocha'
3
+ class ApiTwisterTest < Test::Unit::TestCase
4
+
5
+ context "A class with the default api specification" do
6
+ setup do
7
+ class AttributesOnly
8
+
9
+ def attributes
10
+ {"abc" => nil, "xyz" => nil}
11
+ end
12
+
13
+ def test_method; end
14
+
15
+ include ApiTwister
16
+ define_api
17
+ api :request
18
+ end
19
+
20
+ end
21
+
22
+ should "should only have attributes in the api_hash" do
23
+ assert_equal({:only => [:abc, :xyz]}, AttributesOnly.api_hash(:request))
24
+ end
25
+ end
26
+
27
+ context "A class that defines only the methods as part of the API" do
28
+ setup do
29
+ class KlassWithMethod
30
+ include ApiTwister
31
+
32
+ def attributes
33
+ {"abc" => nil, "xyz" => nil}
34
+ end
35
+
36
+ def test_method; end
37
+
38
+ define_api do |api|
39
+ api.method :test_method
40
+ end
41
+
42
+ api :request
43
+ end
44
+ end
45
+
46
+ should "Have methods and attributes as part of the API because attributes are included by default" do
47
+ assert_equal({:methods => [:test_method], :only => [:abc, :xyz]}, KlassWithMethod.api_hash(:request))
48
+ end
49
+ end
50
+
51
+ context "A class that defines only attributes as a part of the API" do
52
+ setup do
53
+ class KlassWithAttributes
54
+ include ApiTwister
55
+ extend Mocha::API
56
+
57
+ def attributes
58
+ {"abc" => nil, "xyz" => nil}
59
+ end
60
+
61
+ def test_method; end
62
+
63
+ # stub of the ActiveRecord reflect_on_association method
64
+ def self.reflect_on_association(*args)
65
+ association_fake = stub(:nil? => false, :class_name => 'Hash')
66
+ end
67
+
68
+ define_api do |api|
69
+ api.association :another_class
70
+ end
71
+
72
+ api :request
73
+ end
74
+ end
75
+
76
+ should "Have associations and attributes as part of the API because attributes are included by default" do
77
+ assert_equal({:include => {:another_class => {}}, :only => [:abc, :xyz]}, KlassWithAttributes.api_hash(:request))
78
+ end
79
+ end
80
+
81
+ context "A class where a subset of the attributes are defined in the api" do
82
+ setup do
83
+ class KlassWithSubsetOfAttributes
84
+ include ApiTwister
85
+
86
+ def attributes; {"abc" => nil, "xyz" => nil}; end
87
+ def test_method; end
88
+
89
+ define_api do |api|
90
+ api.attribute :abc
91
+ end
92
+
93
+ api :request
94
+ end
95
+ end
96
+
97
+ should "Only have the specified attribute in the api_hash" do
98
+ assert_equal({:only => [:abc]}, KlassWithSubsetOfAttributes.api_hash(:request))
99
+ end
100
+
101
+ should "Include skip types if specified as an api_hash option" do
102
+ assert_equal({:only => [:abc], :skip_types => true}, KlassWithSubsetOfAttributes.api_hash(:request, :skip_types => true))
103
+ end
104
+ end
105
+
106
+ context "ApiDefinition" do
107
+ should("respond to aliased orig_method") { assert ApiTwister::ApiDefinition.new(Hash).respond_to?(:orig_method) }
108
+ should("respond to aliased orig_methods") { assert ApiTwister::ApiDefinition.new(Hash).respond_to?(:orig_methods) }
109
+ end
110
+ end
111
+
@@ -0,0 +1,4 @@
1
+ require 'test/unit'
2
+ require 'shoulda'
3
+ require 'mocha'
4
+ require 'api-twister'
metadata ADDED
@@ -0,0 +1,149 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: api-twister
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 1
9
+ version: 0.0.1
10
+ platform: ruby
11
+ authors:
12
+ - Scott Jacobsen
13
+ - Tee Parham
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-01-19 00:00:00 -07:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: activesupport
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ segments:
30
+ - 2
31
+ - 1
32
+ - 0
33
+ version: 2.1.0
34
+ type: :runtime
35
+ version_requirements: *id001
36
+ - !ruby/object:Gem::Dependency
37
+ name: i18n
38
+ prerelease: false
39
+ requirement: &id002 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ segments:
45
+ - 0
46
+ version: "0"
47
+ type: :development
48
+ version_requirements: *id002
49
+ - !ruby/object:Gem::Dependency
50
+ name: test-unit
51
+ prerelease: false
52
+ requirement: &id003 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ segments:
58
+ - 0
59
+ version: "0"
60
+ type: :development
61
+ version_requirements: *id003
62
+ - !ruby/object:Gem::Dependency
63
+ name: shoulda
64
+ prerelease: false
65
+ requirement: &id004 !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ segments:
71
+ - 0
72
+ version: "0"
73
+ type: :development
74
+ version_requirements: *id004
75
+ - !ruby/object:Gem::Dependency
76
+ name: mocha
77
+ prerelease: false
78
+ requirement: &id005 !ruby/object:Gem::Requirement
79
+ none: false
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ segments:
84
+ - 0
85
+ version: "0"
86
+ type: :development
87
+ version_requirements: *id005
88
+ description: Ruby DSL for non-trivial API specifications
89
+ email:
90
+ - hello@stackpilot.com
91
+ executables: []
92
+
93
+ extensions: []
94
+
95
+ extra_rdoc_files: []
96
+
97
+ files:
98
+ - Gemfile
99
+ - Gemfile.lock
100
+ - README.rdoc
101
+ - Rakefile
102
+ - api-twister.gemspec
103
+ - lib/api-twister.rb
104
+ - lib/api-twister/api_association.rb
105
+ - lib/api-twister/api_attribute.rb
106
+ - lib/api-twister/api_definition.rb
107
+ - lib/api-twister/api_documentation.rb
108
+ - lib/api-twister/api_method.rb
109
+ - lib/api-twister/api_twister.rb
110
+ - lib/api-twister/code_table.rb
111
+ - lib/api-twister/documentation.rb
112
+ - lib/api-twister/version.rb
113
+ - test/test_api_twister.rb
114
+ - test/test_helper.rb
115
+ has_rdoc: true
116
+ homepage: ""
117
+ licenses: []
118
+
119
+ post_install_message:
120
+ rdoc_options: []
121
+
122
+ require_paths:
123
+ - lib
124
+ required_ruby_version: !ruby/object:Gem::Requirement
125
+ none: false
126
+ requirements:
127
+ - - ">="
128
+ - !ruby/object:Gem::Version
129
+ segments:
130
+ - 0
131
+ version: "0"
132
+ required_rubygems_version: !ruby/object:Gem::Requirement
133
+ none: false
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ segments:
138
+ - 0
139
+ version: "0"
140
+ requirements: []
141
+
142
+ rubyforge_project: api-twister
143
+ rubygems_version: 1.3.7
144
+ signing_key:
145
+ specification_version: 3
146
+ summary: Ruby DSL for non-trivial API specifications
147
+ test_files:
148
+ - test/test_api_twister.rb
149
+ - test/test_helper.rb