ddc 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.
@@ -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: