ddc 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f3a6ee066211a5a1664cda8df754f8dd14d6bc07
4
+ data.tar.gz: 23a9ecdcb2c841d6b78e51e1dce499f90197e2ea
5
+ SHA512:
6
+ metadata.gz: dffde5c7259d6cc663a5efcb354b4a4549009a6ea360fedeaf58ff48f4e7a0dd17700deb71fa124e5770bd9e9550cb925c575329d111407abc9501cfce4db85a
7
+ data.tar.gz: fb195d4f69292e9481af00a52d03fe8e2cb8c56cfd4f9fdc713397512448b154e80b6fbea2af0704fdef5a2c803af96760fb900cfa26c7a03751730823888826
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ddc.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Shawn Anderson
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,122 @@
1
+ # DDC
2
+
3
+ DDC (Data Driven Controllers) let's you declare how to wire Rails into your app without the need for code. A Rails controller's job is parsing/interpreting parameters to send to your application domain and taking those results and translating them back out to an HTTP result (html/status/headers). DDC removes the need for all the boiler plate controller code and tests.
4
+
5
+ By adhering to a couple of interfaces, you can avoid writing most controller code and tests. See this [blog post]( http://spin.atomicobject.com/2015/01/26/data-driven-rails-controllers) for more information.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'ddc'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install ddc
22
+
23
+ ## Usage
24
+
25
+ ### Controllers
26
+
27
+ `controllers/monkeys_controller.rb`
28
+
29
+ ```ruby
30
+ DDC::ControllerBuilder.build :monkeys
31
+ before_actions: [:authenticate_user!],
32
+ actions: {
33
+ show: {
34
+ context: 'context_builder#user_and_id',
35
+ service: 'monkey_service#find'
36
+ },
37
+ index: {
38
+ context: 'context_builder#user',
39
+ service: 'monkey_service#find_all'
40
+ },
41
+ update: {
42
+ context: 'context_builder#monkey',
43
+ service: 'monkey_service#update'
44
+ },
45
+ create: {
46
+ context: 'context_builder#monkey',
47
+ service: 'monkey_service#create'
48
+ }
49
+ }
50
+ ```
51
+
52
+ ### Context Builders
53
+ `lib/context_builder.rb`
54
+
55
+ ```ruby
56
+ class ContextBuilder
57
+ def user(context_params)
58
+ HashWithIndifferentAccess.new current_user: context_params[:current_user]
59
+ end
60
+
61
+ def user_and_id(context_params)
62
+ user(context_params).merge(id: context_params[:params][:id])
63
+ end
64
+
65
+ def monkey(context_params)
66
+ info = context_params[:params].permit(monkey: [:color, :poo])
67
+ user_and_id(context_params).merge(info)
68
+ end
69
+ end
70
+ ```
71
+
72
+
73
+ ### Services
74
+
75
+ `lib/monkeys_service.rb`
76
+
77
+ ```ruby
78
+ class MonkeyService
79
+ def find(context)
80
+ id, user = context.values_at :id, :current_user
81
+ me = find_for_user user, id
82
+ if me.present?
83
+ ok(me)
84
+ else
85
+ not_found
86
+ end
87
+ end
88
+
89
+ def update(context)
90
+ id, user, updates = context.values_at :id, :current_user, @model_type
91
+ me = find_for_user user, id
92
+
93
+ translated_updates = translated_cid_to_id(updates)
94
+
95
+ if me.present?
96
+ me.update_attributes translated_updates
97
+ ok(me)
98
+ else
99
+ not_found
100
+ end
101
+ end
102
+
103
+ private
104
+ def not_found
105
+ {status: :not_found}.freeze
106
+ end
107
+ def ok(obj)
108
+ {status: :ok, object: obj}
109
+ end
110
+ end
111
+
112
+ # shortcut for default CRUD service
113
+ MonkeyService = DDC::ServiceBuilder.build(:monkey)
114
+ ```
115
+
116
+ ## Contributing
117
+
118
+ 1. Fork it ( https://github.com/[my-github-username]/ddc/fork )
119
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
120
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
121
+ 4. Push to the branch (`git push origin my-new-feature`)
122
+ 5. Create a new Pull Request
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'ddc/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "ddc"
8
+ spec.version = Ddc::VERSION
9
+ spec.authors = ["Shawn Anderson"]
10
+ spec.email = ["shawn42@gmail.com"]
11
+ spec.summary = %q{Data Driven Controllers for Rails}
12
+ spec.description = %q{Use data to tell Rails how to interact with your domain.}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.7"
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+ spec.add_development_dependency "rspec", "~> 3.0"
24
+ spec.add_dependency "actionpack", "~> 4.1"
25
+ spec.add_dependency "activesupport", "~> 4.1"
26
+
27
+ end
@@ -0,0 +1,6 @@
1
+ require "ddc/version"
2
+ require "ddc/controller_builder"
3
+
4
+ module Ddc
5
+ # Your code goes here...
6
+ end
@@ -0,0 +1,98 @@
1
+ module DDC
2
+ class ControllerBuilder
3
+ DEFAULT_CONTEXT_PARAMS = [:params]
4
+ DEFAULT_STATUSES = {
5
+ ok: 200,
6
+ created: 201,
7
+ not_found: 404,
8
+ not_allowed: 401,
9
+ error: 500
10
+ }
11
+ class << self
12
+ def build_controller(controller_name, config)
13
+ klass = find_or_create_class(controller_name)
14
+ setup_before_actions!(klass, config)
15
+ setup_actions!(controller_name, klass, config)
16
+ klass
17
+ end
18
+
19
+
20
+ def find_or_create_class(controller_name)
21
+ controller_klass_name = controller_name.to_s.camelize+'Controller'
22
+ klass = nil
23
+ if Object.constants.include?(controller_klass_name.to_sym)
24
+ klass = Object.const_get(controller_klass_name)
25
+ else
26
+ klass = Class.new(ApplicationController)
27
+ Object.const_set(controller_klass_name, klass)
28
+ end
29
+ end
30
+
31
+ def setup_before_actions!(klass, config)
32
+ (config[:before_actions] || []).each do |ba|
33
+ klass.before_action ba
34
+ end
35
+ end
36
+
37
+ def setup_actions!(controller_name, klass, config)
38
+ actions = config[:actions]
39
+ raise "Must specify actions" if actions.blank?
40
+
41
+ actions.each do |action, action_desc|
42
+ setup_action! controller_name, klass, action, action_desc
43
+ end
44
+ end
45
+
46
+ def setup_action!(controller_name, klass, action, action_desc)
47
+ raise "Must specify a service for each action" unless action_desc[:service].present?
48
+ raise "Must specify a context for each action" unless action_desc[:context].present?
49
+ proc_klass, proc_method = parse_class_and_method(action_desc[:service])
50
+ context_klass, context_method = parse_class_and_method(action_desc[:context])
51
+
52
+ klass.send :define_method, action do
53
+ context_params = (action_desc[:params] || DEFAULT_CONTEXT_PARAMS).inject({}) do |h, param|
54
+ h[param] = send param
55
+ h
56
+ end
57
+ context = context_klass.new.send(context_method, context_params)
58
+
59
+ result = proc_klass.new.send(proc_method, context)
60
+ obj = result[:object]
61
+ errors = result[:errors] || []
62
+ plural_model_name = controller_name.to_s
63
+ model_name = plural_model_name.singularize
64
+
65
+ # alias in object as model name
66
+ if obj.is_a? Enumerable
67
+ result[plural_model_name] ||= obj
68
+ else
69
+ result[model_name] ||= obj
70
+ end
71
+
72
+ status = DEFAULT_STATUSES.merge(action_desc[:status]||{})[result[:status]]
73
+
74
+ respond_to do |format|
75
+ format.json do
76
+ if obj.nil?
77
+ render json: {errors: errors}, status: status
78
+ else
79
+ render json: obj, status: status
80
+ end
81
+ end
82
+ format.html do
83
+ result.each do |k,v|
84
+ instance_variable_set("@#{k}", v)
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ def parse_class_and_method(str)
92
+ under_klass, method = str.split('#')
93
+ [Object.const_get(under_klass.camelize), method]
94
+ end
95
+ end
96
+ end
97
+
98
+ end
@@ -0,0 +1,11 @@
1
+ module ResponseBuilder
2
+ def not_found
3
+ {status: :not_found}
4
+ end
5
+ def ok(obj)
6
+ {status: :ok, object: obj}
7
+ end
8
+ def created(obj)
9
+ {status: :created, object: obj}
10
+ end
11
+ end
@@ -0,0 +1,54 @@
1
+ module DDC
2
+ class ServiceBuilder
3
+ def self.build(model_type)
4
+ Class.new do
5
+ include ResponseBuilder
6
+ class << self
7
+ attr_accessor :model_type, :ar_model
8
+ end
9
+
10
+ @model_type = model_type
11
+ ar_class_name = model_type.to_s.camelize
12
+ @ar_model = Object.const_get(ar_class_name)
13
+
14
+ def find(context)
15
+ id = context.values_at :id
16
+ me = self.class.ar_model.where id: id
17
+ if me.present?
18
+ ok(me)
19
+ else
20
+ not_found
21
+ end
22
+ end
23
+
24
+ def find_all(context)
25
+ mes = self.class.ar_model.all
26
+ ok(mes)
27
+ end
28
+
29
+ def update(context)
30
+ id, updates = context.values_at :id, self.class.model_type
31
+ me = self.class.ar_model.where id: id
32
+
33
+ if me.present?
34
+ me.update_attributes translated_updates
35
+ ok(me)
36
+ else
37
+ not_found
38
+ end
39
+ end
40
+
41
+ def create(context)
42
+ attributes = context.values_at self.class.model_type
43
+ me = self.class.ar_model.create attributes
44
+ created(me)
45
+ end
46
+
47
+ private
48
+ def find_for_user(user, id)
49
+ return nil if id.nil? || !UUIDUtil.valid?(id)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,3 @@
1
+ module Ddc
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,181 @@
1
+ require 'spec_helper'
2
+
3
+ describe DDC::ControllerBuilder do
4
+ subject { described_class }
5
+
6
+ describe '.build_controller' do
7
+ let(:json_format) {
8
+ format = double('json format')
9
+ expect(format).to receive(:json).and_yield
10
+ expect(format).to receive(:html)
11
+ format }
12
+ let(:html_format) {
13
+ format = double('html format')
14
+ expect(format).to receive(:html).and_yield
15
+ expect(format).to receive(:json)
16
+ format }
17
+ after do
18
+ klass_to_cleanup = :FooController
19
+ Object.send :remove_const, klass_to_cleanup if Object.constants.include?(klass_to_cleanup)
20
+ end
21
+
22
+ it 'defines the controller class' do
23
+ subject.build_controller :foo, actions: {
24
+ index: {
25
+ params: [:current_user, :params],
26
+ context: 'foo_context_builder#bar',
27
+ service: 'baz_service#qux'
28
+ }
29
+ }
30
+ expect(Object.const_get("FooController")).not_to be_nil
31
+ end
32
+
33
+ it 'raises if there are no actions defined' do
34
+ expect(->{subject.build_controller :foo, actions: {}}).to raise_exception
35
+ expect(->{subject.build_controller :foop, {}}).to raise_exception
36
+ end
37
+
38
+ it 'raises if an action is missing context' do
39
+ expect(->{subject.build_controller :foo, actions: {foo: {
40
+ params: [:current_user, :params],
41
+ service: 'baz_service#qux'
42
+ }}}).to raise_exception
43
+ end
44
+
45
+ it 'raises if an action is missing service' do
46
+ expect(->{subject.build_controller :foo, actions: {foo: {
47
+ context: 'foo_context_builder#bar',
48
+ }}}).to raise_exception
49
+ end
50
+
51
+ it 'adds the before actions' do
52
+ class FooController
53
+ def self.before_action(*args);end
54
+ end
55
+
56
+ expect(FooController).to receive(:before_action).with(:my_before_action)
57
+ subject.build_controller :foo,
58
+ before_actions: [:my_before_action],
59
+ actions: {
60
+ index: {
61
+ context: 'foo_context_builder#bar',
62
+ service: 'baz_service#qux'
63
+ }
64
+ }
65
+
66
+ end
67
+
68
+ it 'sunny day get params, process, return object and status, render' do
69
+ class FooController
70
+ def current_user; end
71
+ def some_user; end
72
+ def render(args); end
73
+ def respond_to; end
74
+ end
75
+ controller = FooController.new
76
+
77
+ expect(controller).to receive_messages(
78
+ current_user: :some_user,
79
+ params: {a: :b})
80
+
81
+ render_args = nil
82
+ expect(controller).to receive(:render) do |args|
83
+ render_args = args
84
+ end
85
+ expect(controller).to receive(:respond_to) do |&block|
86
+ block.call(json_format)
87
+ end
88
+ expect_any_instance_of(FooContextBuilder).to receive(:bar).with(hash_including(
89
+ current_user: :some_user,
90
+ params: {a: :b})) { :context }
91
+
92
+ expect_any_instance_of(BazService).to receive(:qux).with(:context) do
93
+ { object: :some_obj, status: :ok }
94
+ end
95
+
96
+ subject.build_controller :foo, actions: {
97
+ index: {
98
+ params: [:current_user, :params],
99
+ context: 'foo_context_builder#bar',
100
+ service: 'baz_service#qux'
101
+ }
102
+ }
103
+ controller.index
104
+
105
+ expect(render_args).to eq(json: :some_obj, status: 200)
106
+ end
107
+
108
+ it 'renders error if service returns nil object' do
109
+ class FooController
110
+ def current_user; end
111
+ def some_user; end
112
+ def render(args); end
113
+ def respond_to; end
114
+ end
115
+
116
+ subject.build_controller :foo, actions: {
117
+ index: {
118
+ params: [:current_user, :params],
119
+ context: 'foo_context_builder#bar',
120
+ service: 'baz_service#qux'
121
+ }
122
+ }
123
+ controller = FooController.new
124
+ expect(controller).to receive_messages(
125
+ current_user: :some_user,
126
+ params: {a: :b})
127
+
128
+ render_args = nil
129
+ expect(controller).to receive(:render) do |args|
130
+ render_args = args
131
+ end
132
+ expect(controller).to receive(:respond_to) do |&block|
133
+ block.call(json_format)
134
+ end
135
+
136
+ expect_any_instance_of(FooContextBuilder).to receive(:bar).with(hash_including(
137
+ current_user: :some_user,
138
+ params: {a: :b})) { :context }
139
+
140
+ expect_any_instance_of(BazService).to receive(:qux).with(:context) do
141
+ { status: :error, errors: ["BOOM"] }
142
+ end
143
+
144
+ controller.index
145
+ expect(render_args).to eq(json: {errors: ["BOOM"]}, status: 500)
146
+ end
147
+
148
+ it 'defines all the action methods' do
149
+ class FooController
150
+ def current_user; end
151
+ def some_user; end
152
+ def render(args); end
153
+ end
154
+ subject.build_controller :foo,
155
+ params: [:current_user, :params],
156
+ actions: {
157
+ index: {
158
+ context: 'foo_context_builder#bar',
159
+ service: 'baz_service#qux'
160
+ },
161
+ other: {
162
+ context: 'foo_context_builder#bar',
163
+ service: 'baz_service#qux'
164
+ }
165
+ }
166
+ controller = FooController.new
167
+ expect(controller).to respond_to(:index)
168
+ expect(controller).to respond_to(:other)
169
+ end
170
+
171
+ class FooContextBuilder
172
+ def bar(opts) {} end
173
+ end
174
+
175
+ class BazService
176
+ def qux(context) {} end
177
+ end
178
+ end
179
+
180
+ end
181
+
@@ -0,0 +1,8 @@
1
+ require 'active_support/all'
2
+ require 'action_controller'
3
+
4
+ class ApplicationController < ActionController::Base
5
+ end
6
+
7
+ require_relative '../lib/ddc'
8
+
metadata ADDED
@@ -0,0 +1,130 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ddc
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Shawn Anderson
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-01-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.7'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: actionpack
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '4.1'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '4.1'
69
+ - !ruby/object:Gem::Dependency
70
+ name: activesupport
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '4.1'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '4.1'
83
+ description: Use data to tell Rails how to interact with your domain.
84
+ email:
85
+ - shawn42@gmail.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - ".gitignore"
91
+ - Gemfile
92
+ - LICENSE.txt
93
+ - README.md
94
+ - Rakefile
95
+ - ddc.gemspec
96
+ - lib/ddc.rb
97
+ - lib/ddc/controller_builder.rb
98
+ - lib/ddc/response_builder.rb
99
+ - lib/ddc/service_builder.rb
100
+ - lib/ddc/version.rb
101
+ - spec/controller_builder_spec.rb
102
+ - spec/spec_helper.rb
103
+ homepage: ''
104
+ licenses:
105
+ - MIT
106
+ metadata: {}
107
+ post_install_message:
108
+ rdoc_options: []
109
+ require_paths:
110
+ - lib
111
+ required_ruby_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ required_rubygems_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ requirements: []
122
+ rubyforge_project:
123
+ rubygems_version: 2.2.2
124
+ signing_key:
125
+ specification_version: 4
126
+ summary: Data Driven Controllers for Rails
127
+ test_files:
128
+ - spec/controller_builder_spec.rb
129
+ - spec/spec_helper.rb
130
+ has_rdoc: