thalamus 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 822ad009daebff4c13eac447e6724b0f2206641e
4
- data.tar.gz: bd1d9d475aff1910b645372f2c88d9e900ec9d06
3
+ metadata.gz: dbbccc5279014f01d693e7dd49bb79d5af7d9a3c
4
+ data.tar.gz: f7001941bd457f0f7dfa8da64c33c725fd2d644b
5
5
  SHA512:
6
- metadata.gz: f1bcd0fc04e006f5116ed0348ed3fc5789e57d432562725eafb479b94325140edc250f5f460472c58fe4f2a422285478550614681f51770da511bab8d334b3c3
7
- data.tar.gz: 3bbd55cca05ce32b5112dc5bd573c5c54bd50a38d497b8c00da3133310f7dc33f83250412c1bd00cf394e4c2e7db79cf9981e8769cbad03f8dacedb2d0e0a84a
6
+ metadata.gz: cb17a44901fe0258c7322c916ca6d57bdf63cf4c244deff6baf761eeecd6ad7bb02496fbbd5ef0fdf0d05b62a56549d09395aa9cbdf7c8de2bb39a553cc3fa94
7
+ data.tar.gz: a6aae1401cbc2b69e0685023b4f2d06bac17d7f0035f8b85400ef1f00a7bcd93ef256ee7ec2feec44641bb83f75b25c210fc0abb71183e645e84a0a67f0e1fe4
data/Gemfile CHANGED
@@ -1,2 +1,14 @@
1
1
  source "https://rubygems.org"
2
2
  gemspec
3
+
4
+ group :development do
5
+ gem 'pry'
6
+ gem 'guard'
7
+ gem 'guard-minitest'
8
+ gem 'rake-notes'
9
+ end
10
+
11
+ group :test do
12
+ gem 'minitest', '~> 5.0'
13
+ gem 'minitest-reporters'
14
+ end
data/README.md CHANGED
@@ -1,20 +1,26 @@
1
1
  # Thalamus
2
2
 
3
- **Thalamus** is a simple implementation of the interactor pattern. It encourages SOLID(ish) design, in particular the separation of business logic from implementation logic. Thin files, fat directories!
3
+ [![Gem Version](https://badge.fury.io/rb/thalamus.svg)](https://badge.fury.io/rb/thalamus)
4
4
 
5
- Thalamus is heavily inspired by [Hanami::Interactor](https://github.com/hanami/utils) and, in many ways, is just a more opinionated version.
5
+ **Thalamus** is a simple implementation of the interactor pattern. It encourages separating business logic from implementation concerns. Thalamus provides the glue between your endpoints and your problem domain, and is especially useful for keeping logic consistent in apps with multiple interfaces.
6
6
 
7
7
  ## Installation
8
8
 
9
- Come on now.
9
+ Come on now:
10
10
 
11
11
  ```ruby
12
- gem 'thalamus'#, github: 'joshgreenberg/thalamus'
12
+ gem 'thalamus'
13
13
  ```
14
14
  ```bash
15
15
  bundle install
16
16
  ```
17
17
 
18
+ Or:
19
+
20
+ ```bash
21
+ gem install thalamus
22
+ ```
23
+
18
24
  ## Usage
19
25
 
20
26
  ```ruby
@@ -30,7 +36,7 @@ class DoSomething < Thalamus::Interactor
30
36
  end
31
37
  ```
32
38
 
33
- Call `DoSomething.call(payload)` from anywhere - a controller, an API endpoint, a task or job, the command line, another interactor, etc. This is the _only_ entry point from your app's implementation details to the business logic.
39
+ Call `DoSomething.call(payload)` from anywhere - a controller, an API endpoint, a task or job, the command line, another interactor, etc. This is the _only_ entry point from your app's implementation details into the business logic.
34
40
 
35
41
  Note that you invoke with `Interactor.call` but define behaviour with `Interactor#call`.
36
42
 
@@ -38,26 +44,44 @@ Note that you invoke with `Interactor.call` but define behaviour with `Interacto
38
44
 
39
45
  The input payload must be a hash and is immutable. Symbolized keys are preferred. The hash content may take two forms:
40
46
 
41
- - `{params: {foo: 'bar'}, current_user: User.new}` is recommended. `:params` must be structured like a JSON object. Often, implementation in the controller will look like `Interactor.call(params: http_request_body, current_user: authenticated_user)`.
47
+ - `{params: {foo: 'bar'}, current_user: User.new}` is recommended. Often, implementation in the controller will look like `Interactor.call(params: http_request_params_hash, current_user: authenticated_user)`.
42
48
  - `{foo: 'bar', current_user: User.new}` is also accepted. Special fields will be extracted and will _not_ be passed to `:params`. Special fields include:
43
49
  + `:current_user` (see Authorization)
44
50
 
45
51
  ### Authorization
46
52
 
47
53
  ```ruby
54
+ class MyPolicy < Thalamus::Policy
55
+ def eat?
56
+ # boolean expression
57
+ end
58
+ end
59
+
48
60
  class EatCheese < Thalamus::Interactor
49
- policy PunditStylePolicy, :eat?
61
+ policy MyPolicy, :eat?
50
62
  def call
51
63
  # ...
52
64
  end
53
65
  end
54
66
  ```
55
67
 
56
- If a policy is defined, `.call` will extract `payload[:current_user]` (which should be an entity) and run it through the policy. If the policy fails, the interactor will fail immediately.
68
+ If a policy is defined on the interactor, `Interactor.call` provides its payload to the policy. If the policy fails, the interactor will fail immediately.
69
+
70
+ Policies have access to `#current_user` and `#params` getters.
71
+
72
+ In your policy, you may need to define a way to transform your data from raw inputs to rich domain objects:
73
+
74
+ ```ruby
75
+ class ApplicationPolicy < Thalamus::Policy
76
+ def model
77
+ params[:type].constantize.find(params[:id])
78
+ end
79
+ end
80
+ ```
57
81
 
58
82
  ### Validation
59
83
 
60
- Thalamus depends on [dry-rb](http://dry-rb.org/gems/dry-validation) and [Hanami::Validations](https://github.com/hanami/validations):
84
+ Thalamus depends on [dry-rb](http://dry-rb.org/gems/dry-validation) via [Hanami::Validations](https://github.com/hanami/validations):
61
85
 
62
86
  ```ruby
63
87
  class Blah < Thalamus::Interactor
@@ -88,23 +112,26 @@ end
88
112
 
89
113
  ### Execution
90
114
 
91
- This is where you work with domain entities - and as a best practice, this should be the outermost layer _in your entire application_ where you work with them. (User authentication in the router or controller is the only exception, and even then you should only be exposing credentials outside of the business domain.) `#call` will only be run if the interactor is both authorized and valid, enforcing permitted access and changes.
115
+ This is where you work with domain entities - and as a best practice, this should be the outermost layer _in your entire application_ where you work with them. (User _authentication_ in the router or controller is the only exception, and even then you should only be exposing credentials outside of the business domain.) `#call` will only be run if the interactor is both authorized and valid, enforcing permitted access and changes.
92
116
 
93
117
  `#error!(message)` triggers a failure and aborts the interactor.
94
118
 
95
- `#params` provides a copy of the sanitized input after it has been processed by the validator. It cannot be modified - see below to mutate return data.
119
+ `#current_user` and `#params` access the payload. `#params` provides a copy of the sanitized input after it has been processed by the validator. It cannot be modified - see Output to mutate return data.
96
120
 
97
121
  ### Output
98
122
 
99
- An interactor returns an instance of Thalamus::Result. It responds to the following instance methods:
123
+ An interactor returns an instance of `Thalamus::Result`. It responds to the following instance methods:
100
124
 
101
125
  - `status` => Symbol in `[:success, :unauthorized, :invalid, :failure]`
102
- + `success?` => Boolean, results are successful if no errors are raised
103
- + `unauthorized?` => Boolean
104
- + `invalid?` => Boolean
105
- + `failure?` => Boolean, equivalent of `!success?`
106
- - `errors` => Array of `Interactor#error!(message)`
107
- - `validation_errors` => Hash of error messages from dry-validation, organized by key
126
+ + `success?` => Boolean, results are successful if no errors are raised
127
+ + `unauthorized?` => Boolean
128
+ + `invalid?` => Boolean
129
+ + `failure?` => Boolean, equivalent of `!success?`
130
+ - `errors` => Enumerable of error messages, depending on status
131
+ + success: Empty array: `[]`
132
+ + unauthorized: `['Unauthorized']`
133
+ + invalid: Hash of arrays from dry-validation: `{param: [errors]}`
134
+ + failure: `[error!(message)]`
108
135
 
109
136
  Any other name starting with a lowercase letter can be exposed to the result object:
110
137
 
@@ -120,4 +147,11 @@ end
120
147
 
121
148
  Whatever is at `@book` is now available at `Result#book`.
122
149
 
123
- It is up to the developer to enforce boundaries appropriately. You must decide which data to expose outside of the interactor, and which layer is responsible for converting it. For example, you may wish to convert an entity to JSON for an API response, but when calling an interactor from somewhere else you may find it more useful to return a richer object. You may decide to handle JSON conversions in the controller - the interactor is the layer that modifies data, but there is no conversion mechanism because it is agnostic to how it is stored.
150
+ It is up to the developer to enforce boundaries appropriately. You must decide which data to expose outside of the interactor, and which layer is responsible for converting it. For example, you may wish to convert an entity to JSON for an API response, but when calling an interactor from somewhere else you may find it more useful to return a richer object. You may decide to handle JSON conversions in the controller - the interactor is the layer that modifies data, but there is no conversion mechanism because it is agnostic to how it is used.
151
+
152
+ For your convenience, `Result#response` provides a JSON-friendly hash of appropriate information:
153
+
154
+ - success: all exposures
155
+ - unauthorized: `{messages: ['Unauthorized']}`
156
+ - invalid: errors hash
157
+ - failure: `{messages: [error_message]}`
data/TODO.md ADDED
@@ -0,0 +1,3 @@
1
+ # TODO
2
+
3
+ - Allow custom validation library
@@ -1,6 +1,5 @@
1
- require 'thalamus/version'
2
- require 'thalamus/interactor'
3
- require 'thalamus/policy'
4
-
5
1
  module Thalamus
6
2
  end
3
+
4
+ require 'thalamus/interactor'
5
+ require 'thalamus/policy'
@@ -1,27 +1,28 @@
1
1
  require 'hanami/validations/form'
2
- require 'thalamus/interactor/result'
2
+ require 'thalamus/result'
3
3
 
4
4
  module Thalamus
5
5
  class Interactor
6
6
  include Hanami::Validations::Form
7
7
 
8
+ attr_reader :current_user
9
+
8
10
  def initialize(payload = {})
9
11
  @current_user = payload.delete(:current_user)
10
12
  @_params = payload.delete(:params) || payload
11
13
  @_errors = []
12
14
  end
13
- attr_reader :current_user, :model
14
15
 
15
16
  def params
16
17
  @_params.dup
17
18
  end
18
19
 
19
20
  def _call
20
- status = catch :status do
21
+ status = catch :status do
21
22
  # Authorization
22
23
  if tuple = self.class._policy
23
24
  klass, policy = tuple
24
- fail!(:unauthorized) unless klass.new(current_user, model).send(policy)
25
+ fail!(:unauthorized) unless klass.new(current_user, params).send(policy)
25
26
  end
26
27
 
27
28
  # Validation
@@ -31,29 +32,30 @@ module Thalamus
31
32
  call
32
33
  :success
33
34
  end
34
- result = Result.new.expose!(_exposures)
35
- .finish!(status, @_errors, @_validation_errors)
35
+ result = Result.new(status, @_errors, _exposures)
36
36
  yield result if block_given? && result.success?
37
37
  result
38
38
  end
39
39
 
40
40
  private
41
41
 
42
+ # Throw an error from the business logic, and abort
42
43
  def error!(msg = '')
43
- @_errors << msg
44
+ @_errors = [msg]
44
45
  fail!
45
46
  end
46
47
 
48
+ # Internal methods
47
49
  def fail!(status = :failure)
48
50
  throw :status, status
49
51
  end
50
52
 
51
53
  def valid?
52
54
  if schema = self.class.schema
53
- r = schema.call(@_params)
54
- @_params = r.output
55
- @_validation_errors = r.errors
56
- r.success?
55
+ validation = schema.call(@_params)
56
+ @_params = validation.output
57
+ @_errors = validation.errors
58
+ validation.success?
57
59
  else
58
60
  true
59
61
  end
@@ -66,36 +68,42 @@ module Thalamus
66
68
  end.to_h
67
69
  end
68
70
 
71
+ # Class methods
69
72
  class << self
70
73
  private :new
71
74
 
72
- def call(payload = {}, &block) # Public entry point
75
+ attr_reader :_policy, :exposures
76
+
77
+ # Public entry point
78
+ def call(payload = {}, &block)
73
79
  new(payload)._call(&block)
74
80
  end
75
81
 
76
- attr_reader :_policy, :exposures
77
-
78
82
  private # Class-level DSL
79
83
 
84
+ # Example:
85
+ # policy UserPolicy, :edit?
80
86
  def policy(klass, policy)
81
87
  @_policy = [klass, policy]
82
88
  end
83
89
 
84
- def schema_type(type)
85
- @_schema_type = type
86
- end
87
-
88
- def _schema_type
89
- @_schema_type || super
90
- end
91
-
90
+ # Example:
91
+ # expose :foo, :bar
92
92
  def expose(*ivars)
93
93
  ivars.each do |ivar|
94
94
  @exposures ||= []
95
95
  @exposures << ivar.to_sym
96
96
  end
97
97
  end
98
- end
99
98
 
100
- end
99
+ # TODO: Document this
100
+ # def schema_type(type)
101
+ # @_schema_type = type
102
+ # end
103
+
104
+ # def _schema_type
105
+ # @_schema_type || super
106
+ # end
107
+ end # class << self
108
+ end # class Interactor
101
109
  end
@@ -1,9 +1,9 @@
1
1
  module Thalamus
2
2
  class Policy
3
- attr_reader :user, :model
3
+ attr_reader :current_user, :params
4
4
 
5
- def initialize(user, model)
6
- @user, @model = user, model
5
+ def initialize(current_user, params)
6
+ @current_user, @params = current_user, params
7
7
  end
8
8
 
9
9
  def method_missing(m, *args, &block)
@@ -0,0 +1,55 @@
1
+ module Thalamus
2
+ class Result
3
+ def initialize(status, errors, exposures)
4
+ @status = status
5
+ @errors = errors || []
6
+ @exposures = exposures || {}
7
+ end
8
+
9
+ attr_reader :status, :errors
10
+
11
+ def success?
12
+ status == :success && errors.empty?
13
+ end
14
+
15
+ def unauthorized?
16
+ status == :unauthorized
17
+ end
18
+
19
+ def invalid?
20
+ status == :invalid
21
+ end
22
+
23
+ def failure?
24
+ !success?
25
+ end
26
+
27
+ def response
28
+ if success?
29
+ @exposures
30
+ elsif unauthorized?
31
+ {messages: ['Unauthorized']}
32
+ elsif invalid?
33
+ errors
34
+ elsif failure?
35
+ {messages: errors}
36
+ else
37
+ raise "[Thalamus] Invalid result status: #{status}"
38
+ end
39
+ end
40
+
41
+ protected
42
+
43
+ def method_missing(m, *args, &block)
44
+ if m.to_s =~ /^[a-z]/
45
+ @exposures.fetch(m) { super }
46
+ else
47
+ super
48
+ end
49
+ end
50
+
51
+ def respond_to_missing?(m, include_private = false)
52
+ m.to_s =~ /^[a-z]/ && @exposures.key?(m)
53
+ end
54
+ end # class Result
55
+ end
@@ -1,3 +1,14 @@
1
1
  module Thalamus
2
- VERSION = "0.0.2"
2
+ def self.version
3
+ Gem::Version.new VERSION::STRING
4
+ end
5
+
6
+ module VERSION
7
+ MAJOR = 0
8
+ MINOR = 1
9
+ TINY = 0
10
+ PRE = nil
11
+
12
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
13
+ end
3
14
  end
@@ -1,11 +1,11 @@
1
1
  # coding: utf-8
2
2
  lib = File.expand_path("../lib", __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ $:.unshift(lib) unless $:.include?(lib)
4
4
  require "thalamus/version"
5
5
 
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = "thalamus"
8
- spec.version = Thalamus::VERSION
8
+ spec.version = Thalamus.version
9
9
  spec.authors = ["Josh Greenberg"]
10
10
  spec.email = ["joshgreenberg91@gmail.com"]
11
11
 
@@ -22,12 +22,6 @@ Gem::Specification.new do |spec|
22
22
 
23
23
  spec.add_development_dependency "bundler", "~> 1.15"
24
24
  spec.add_development_dependency "rake", "~> 10.0"
25
- spec.add_development_dependency "minitest", "~> 5.0"
26
- spec.add_development_dependency "minitest-reporters"
27
- spec.add_development_dependency "pry"
28
- spec.add_development_dependency "guard"
29
- spec.add_development_dependency "guard-minitest"
30
- spec.add_development_dependency "rake-notes"
31
-
25
+
32
26
  spec.add_dependency "hanami-validations", "~> 1.0"
33
27
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: thalamus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Josh Greenberg
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-07-05 00:00:00.000000000 Z
11
+ date: 2017-09-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -38,90 +38,6 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '10.0'
41
- - !ruby/object:Gem::Dependency
42
- name: minitest
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: '5.0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: '5.0'
55
- - !ruby/object:Gem::Dependency
56
- name: minitest-reporters
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: '0'
69
- - !ruby/object:Gem::Dependency
70
- name: pry
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - ">="
74
- - !ruby/object:Gem::Version
75
- version: '0'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: '0'
83
- - !ruby/object:Gem::Dependency
84
- name: guard
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- version: '0'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - ">="
95
- - !ruby/object:Gem::Version
96
- version: '0'
97
- - !ruby/object:Gem::Dependency
98
- name: guard-minitest
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - ">="
102
- - !ruby/object:Gem::Version
103
- version: '0'
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - ">="
109
- - !ruby/object:Gem::Version
110
- version: '0'
111
- - !ruby/object:Gem::Dependency
112
- name: rake-notes
113
- requirement: !ruby/object:Gem::Requirement
114
- requirements:
115
- - - ">="
116
- - !ruby/object:Gem::Version
117
- version: '0'
118
- type: :development
119
- prerelease: false
120
- version_requirements: !ruby/object:Gem::Requirement
121
- requirements:
122
- - - ">="
123
- - !ruby/object:Gem::Version
124
- version: '0'
125
41
  - !ruby/object:Gem::Dependency
126
42
  name: hanami-validations
127
43
  requirement: !ruby/object:Gem::Requirement
@@ -150,12 +66,12 @@ files:
150
66
  - LICENSE.txt
151
67
  - README.md
152
68
  - Rakefile
69
+ - TODO.md
153
70
  - bin/console
154
- - bin/setup
155
71
  - lib/thalamus.rb
156
72
  - lib/thalamus/interactor.rb
157
- - lib/thalamus/interactor/result.rb
158
73
  - lib/thalamus/policy.rb
74
+ - lib/thalamus/result.rb
159
75
  - lib/thalamus/version.rb
160
76
  - thalamus.gemspec
161
77
  homepage:
data/bin/setup DELETED
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install
7
-
8
- # Do any other automated setup that you need to do here
@@ -1,64 +0,0 @@
1
- module Thalamus
2
- class Interactor
3
- class Result
4
-
5
- def expose!(exposures)
6
- @exposures = exposures
7
- self
8
- end
9
-
10
- def finish!(status, errors = [], validation_errors = {})
11
- @status = status
12
- @errors = errors
13
- @validation_errors = validation_errors
14
- self
15
- end
16
- attr_reader :status, :errors, :validation_errors
17
-
18
- def success? # 200 or 201
19
- @status == :success && errors.empty?
20
- end
21
-
22
- def unauthorized? # 403
23
- @status == :unauthorized
24
- end
25
-
26
- def invalid? # 422
27
- @status == :invalid
28
- end
29
-
30
- def failure? # catch-all
31
- !success?
32
- end
33
-
34
- def response
35
- if success?
36
- @exposures
37
- elsif unauthorized?
38
- {errors: ["UNAUTHORIZED"]}
39
- elsif invalid?
40
- validation_errors
41
- elsif failure?
42
- {errors: errors}
43
- else
44
- raise "[Thalamus] Invalid result status: #{status}"
45
- end
46
- end
47
-
48
- protected
49
-
50
- def method_missing(m, *args, &block)
51
- if m.to_s =~ /^[a-z]/
52
- (@exposures || {}).fetch(m) { super }
53
- else
54
- super
55
- end
56
- end
57
-
58
- def respond_to_missing?(m, include_private = false)
59
- m.to_s =~ /^[a-z]/ && @exposures.key?(m)
60
- end
61
-
62
- end
63
- end
64
- end