surrounded 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/workflows/codeql-analysis.yml +70 -0
- data/.github/workflows/test.yml +18 -0
- data/Changelog.md +9 -0
- data/Gemfile +0 -1
- data/LICENSE.txt +1 -1
- data/README.md +36 -6
- data/examples/bottles.rb +107 -117
- data/lib/surrounded/access_control.rb +9 -17
- data/lib/surrounded/context/initializing.rb +2 -1
- data/lib/surrounded/context/negotiator.rb +6 -5
- data/lib/surrounded/context/role_builders.rb +1 -0
- data/lib/surrounded/context/role_map.rb +16 -7
- data/lib/surrounded/context/seclusion.rb +20 -0
- data/lib/surrounded/context/trigger_controls.rb +3 -3
- data/lib/surrounded/context.rb +4 -17
- data/lib/surrounded/shortcuts.rb +16 -4
- data/lib/surrounded/version.rb +2 -2
- data/surrounded.gemspec +0 -1
- data/test/context_shortcuts_test.rb +26 -1
- data/test/example_proxy_test.rb +1 -1
- data/test/initialization_test.rb +1 -1
- data/test/role_context_method_test.rb +37 -1
- data/test/test_helper.rb +3 -1
- metadata +9 -22
- data/.travis.yml +0 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 152c9c728084be8069384cf825cedf5ab827d4fd28b0710cbdd19229d8284bc7
|
4
|
+
data.tar.gz: 82a50b285aa5328084481c438d78f8ed3e17ba362bcad45c14efaf5ad1bf0fc3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9f7cf841bf2c4ceeb27fd6bd2f11c4041cad7c28e1ce71574b5bbc1c201b0944a2481966b1c2349b3cbfd465a98f7627f1621fae0e0e8d2c2c9fcde42859577f
|
7
|
+
data.tar.gz: aa91c16fefb0cdc19f1b597d2fffd876c2f0bce15bcbcaacb3a713570acac350d8ff434a8c3d6177d4c9e1a79c620611f012d1aff2bcd24fef10ddf696bcd13b
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# For most projects, this workflow file will not need changing; you simply need
|
2
|
+
# to commit it to your repository.
|
3
|
+
#
|
4
|
+
# You may wish to alter this file to override the set of languages analyzed,
|
5
|
+
# or to provide custom queries or build logic.
|
6
|
+
#
|
7
|
+
# ******** NOTE ********
|
8
|
+
# We have attempted to detect the languages in your repository. Please check
|
9
|
+
# the `language` matrix defined below to confirm you have the correct set of
|
10
|
+
# supported CodeQL languages.
|
11
|
+
#
|
12
|
+
name: "CodeQL"
|
13
|
+
|
14
|
+
on:
|
15
|
+
push:
|
16
|
+
branches: [ master ]
|
17
|
+
pull_request:
|
18
|
+
# The branches below must be a subset of the branches above
|
19
|
+
branches: [ master ]
|
20
|
+
schedule:
|
21
|
+
- cron: '41 18 * * 4'
|
22
|
+
|
23
|
+
jobs:
|
24
|
+
analyze:
|
25
|
+
name: Analyze
|
26
|
+
runs-on: ubuntu-latest
|
27
|
+
permissions:
|
28
|
+
actions: read
|
29
|
+
contents: read
|
30
|
+
security-events: write
|
31
|
+
|
32
|
+
strategy:
|
33
|
+
fail-fast: false
|
34
|
+
matrix:
|
35
|
+
language: [ 'ruby' ]
|
36
|
+
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
37
|
+
# Learn more about CodeQL language support at https://git.io/codeql-language-support
|
38
|
+
|
39
|
+
steps:
|
40
|
+
- name: Checkout repository
|
41
|
+
uses: actions/checkout@v3
|
42
|
+
|
43
|
+
# Initializes the CodeQL tools for scanning.
|
44
|
+
- name: Initialize CodeQL
|
45
|
+
uses: github/codeql-action/init@v2
|
46
|
+
with:
|
47
|
+
languages: ${{ matrix.language }}
|
48
|
+
# If you wish to specify custom queries, you can do so here or in a config file.
|
49
|
+
# By default, queries listed here will override any specified in a config file.
|
50
|
+
# Prefix the list here with "+" to use these queries and those in the config file.
|
51
|
+
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
52
|
+
|
53
|
+
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
54
|
+
# If this step fails, then you should remove it and run the build manually (see below)
|
55
|
+
- name: Autobuild
|
56
|
+
uses: github/codeql-action/autobuild@v2
|
57
|
+
|
58
|
+
# ℹ️ Command-line programs to run using the OS shell.
|
59
|
+
# 📚 https://git.io/JvXDl
|
60
|
+
|
61
|
+
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
62
|
+
# and modify them (or add more) to build your code if your project
|
63
|
+
# uses a compiled language
|
64
|
+
|
65
|
+
#- run: |
|
66
|
+
# make bootstrap
|
67
|
+
# make release
|
68
|
+
|
69
|
+
- name: Perform CodeQL Analysis
|
70
|
+
uses: github/codeql-action/analyze@v2
|
@@ -0,0 +1,18 @@
|
|
1
|
+
name: spec
|
2
|
+
|
3
|
+
on: push
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
build:
|
7
|
+
runs-on: ubuntu-latest
|
8
|
+
strategy:
|
9
|
+
matrix:
|
10
|
+
ruby: [ '2.7', '3.0', '3.1' ]
|
11
|
+
name: Ruby ${{ matrix.ruby }} sample
|
12
|
+
steps:
|
13
|
+
- uses: actions/checkout@v2
|
14
|
+
- uses: ruby/setup-ruby@v1
|
15
|
+
with:
|
16
|
+
ruby-version: ${{ matrix.ruby }}
|
17
|
+
bundler-cache: true
|
18
|
+
- run: bundle exec rake
|
data/Changelog.md
CHANGED
@@ -2,6 +2,15 @@
|
|
2
2
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
4
4
|
|
5
|
+
## [1.1.0]
|
6
|
+
|
7
|
+
- Dropped support for Ruby below 2.7 to take advantage of the ellipsis for argument forwarding
|
8
|
+
- Memoize the Negotiator methods for interface
|
9
|
+
|
10
|
+
## [1.0.1]
|
11
|
+
|
12
|
+
- Fix a bug where shortcut_triggers would not work with keyword initialize
|
13
|
+
|
5
14
|
## [1.0.0]
|
6
15
|
|
7
16
|
- Drop deprecations around Context initialize method. It now requires keyword arguments. Non-keyword argumennts may be used with initialize_without_keywords
|
data/Gemfile
CHANGED
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# ![Surrounded](http://saturnflyer.github.io/surrounded/images/surrounded.png "Surrounded")
|
2
2
|
## Be in control of business logic.
|
3
3
|
|
4
|
-
[![Build Status](https://
|
4
|
+
[![Build Status](https://github.com/saturnflyer/surrounded/actions/workflows/test.yml/badge.svg)](https://github.com/saturnflyer/surrounded/actions)
|
5
5
|
[![Code Climate](https://codeclimate.com/github/saturnflyer/surrounded.png)](https://codeclimate.com/github/saturnflyer/surrounded)
|
6
6
|
|
7
7
|
Surrounded is designed to help you better manage your business logic by keeping cohesive behaviors together. Bring objects together to implement your use cases and gain behavior only when necessary.
|
@@ -53,13 +53,13 @@ There are 2 things left to do:
|
|
53
53
|
|
54
54
|
Initializing contexts does not require the use of keyword arguments, but you may opt out.
|
55
55
|
|
56
|
-
You should consider using explicit names when
|
56
|
+
You should consider using explicit names when initializing now by using `initialize_without_keywords`:
|
57
57
|
|
58
58
|
```ruby
|
59
59
|
class Employment
|
60
60
|
extend Surrounded::Context
|
61
61
|
|
62
|
-
|
62
|
+
initialize_without_keywords :employee, :boss
|
63
63
|
end
|
64
64
|
|
65
65
|
user1 = User.find(1)
|
@@ -636,6 +636,22 @@ class Organization
|
|
636
636
|
end
|
637
637
|
```
|
638
638
|
|
639
|
+
If you want to change the way the singular verson of a role is used, override `singularize_name`:
|
640
|
+
|
641
|
+
```ruby
|
642
|
+
class Organization
|
643
|
+
extend Surrounded::Context
|
644
|
+
|
645
|
+
def singularize_name(name)
|
646
|
+
if name == "my special rule"
|
647
|
+
# do your thing
|
648
|
+
else
|
649
|
+
super # use the default
|
650
|
+
end
|
651
|
+
end
|
652
|
+
end
|
653
|
+
```
|
654
|
+
|
639
655
|
## Reusing context objects
|
640
656
|
|
641
657
|
If you create a context object and need to use the same type of object with new role players, you may use the `rebind` method. It will clear any instance_variables from your context object and map the given objects to their names:
|
@@ -824,7 +840,7 @@ context = ActiviatingAccount.new(some_object, some_account)
|
|
824
840
|
context.triggers # => lists a Set of triggers
|
825
841
|
# when using protect_triggers
|
826
842
|
context.triggers # => lists a Set of triggers which may currently be called
|
827
|
-
context.
|
843
|
+
context.all_triggers # => lists a Set of all triggers (the same as if protect_triggers was _not_ used)
|
828
844
|
context.allow?(:trigger_name) # => returns a boolean if the trigger may be run
|
829
845
|
|
830
846
|
# reuse the context object with new role players
|
@@ -835,6 +851,20 @@ context.rebind(activator: another_object, account: another_account)
|
|
835
851
|
|
836
852
|
The dependencies are minimal. The plan is to keep it that way but allow you to configure things as you need. The [Triad](http://github.com/saturnflyer/triad) project was written specifically to manage the mapping of roles and objects to the modules which contain the behaviors. It is used in Surrounded to keep track of role player, roles, and role constant names but it is not a hard requirement. You may implement your own but presently you'll need to dive into the implementation to fully understand how. Future updates may provide better support and guidance.
|
837
853
|
|
854
|
+
If you want to override the class used for mapping roles to behaviors, override the `role_map` method.
|
855
|
+
|
856
|
+
```ruby
|
857
|
+
class MyContext
|
858
|
+
extend Surrounded::Context
|
859
|
+
|
860
|
+
def role_map
|
861
|
+
@container ||= role_mapper_class.new(base: MySpecialDataContainer)
|
862
|
+
end
|
863
|
+
end
|
864
|
+
```
|
865
|
+
|
866
|
+
The class you provide will be initialized with `new` and is expected to implement the methods: `:update`, `:each`, `:values`, and `:keys`.
|
867
|
+
|
838
868
|
If you're using [Casting](http://github.com/saturnflyer/casting), for example, Surrounded will attempt to use that before extending an object, but it will still work without it.
|
839
869
|
|
840
870
|
## Support for other ways to apply behavior
|
@@ -918,9 +948,9 @@ class MyCustomContext
|
|
918
948
|
end
|
919
949
|
```
|
920
950
|
|
921
|
-
You can remember the method name by the convention that `remove` or `apply` describes it's function, `behavior` refers to the first argument (
|
951
|
+
You can remember the method name by the convention that `remove` or `apply` describes it's function, `behavior` refers to the first argument (the constant holding the behaviors), and then the name of the role which refers to the role playing object: `remove_behavior_role`.
|
922
952
|
|
923
|
-
##Name collisions between methods and roles
|
953
|
+
## Name collisions between methods and roles
|
924
954
|
|
925
955
|
Lets say that you wish to create a context as below, intending to use instances of the following two classes as role players:
|
926
956
|
|
data/examples/bottles.rb
CHANGED
@@ -1,145 +1,135 @@
|
|
1
|
-
require
|
1
|
+
require "surrounded"
|
2
|
+
class CountdownSong
|
3
|
+
def initialize(max:, min:)
|
4
|
+
@max = max
|
5
|
+
@min = min
|
6
|
+
end
|
7
|
+
attr_reader :max, :min
|
8
|
+
|
9
|
+
def sing_with(verse_template)
|
10
|
+
max.downto(min).map { |num| verse(num, verse_template) }
|
11
|
+
end
|
12
|
+
|
13
|
+
def verse(number, verse_template)
|
14
|
+
verse_template.lyrics(number)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class BottleVerse
|
19
|
+
def self.lyrics(number)
|
20
|
+
new(bottle_number: number).lyrics
|
21
|
+
end
|
2
22
|
|
3
|
-
class Countdown
|
4
23
|
extend Surrounded::Context
|
5
|
-
|
6
|
-
initialize :
|
7
|
-
|
8
|
-
trigger :
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
end
|
19
|
-
|
20
|
-
role :singer do
|
21
|
-
def start
|
22
|
-
announce_full_status
|
23
|
-
take_action
|
24
|
-
end
|
25
|
-
|
26
|
-
def continue
|
27
|
-
announce_status
|
28
|
-
pause
|
29
|
-
start
|
30
|
-
end
|
31
|
-
|
32
|
-
def announce_full_status
|
33
|
-
output %{#{location.status}, #{location.inventory}}.capitalize
|
34
|
-
end
|
35
|
-
|
36
|
-
def announce_status
|
37
|
-
output %{#{location.status}}.capitalize
|
24
|
+
|
25
|
+
initialize :bottle_number
|
26
|
+
|
27
|
+
trigger :lyrics do
|
28
|
+
"#{bottle_number} of beer on the wall, ".capitalize +
|
29
|
+
"#{bottle_number} of beer.\n" +
|
30
|
+
"#{bottle_number.action}, " +
|
31
|
+
"#{bottle_number.successor} of beer on the wall.\n"
|
32
|
+
end
|
33
|
+
|
34
|
+
role :bottle_number, :wrapper do
|
35
|
+
def self.handles(number)
|
36
|
+
BottleVerse.register_bottle_role(number, self)
|
38
37
|
end
|
39
|
-
|
40
|
-
def
|
41
|
-
|
42
|
-
output %{Go to the store and get some more}
|
43
|
-
next_part.finish
|
44
|
-
else
|
45
|
-
output %{#{location.subtraction}, pass it around}.capitalize
|
46
|
-
next_part.continue
|
47
|
-
end
|
38
|
+
|
39
|
+
def to_s
|
40
|
+
"#{quantity} #{container}"
|
48
41
|
end
|
49
|
-
|
50
|
-
def
|
51
|
-
|
42
|
+
|
43
|
+
def quantity
|
44
|
+
__getobj__.to_s
|
52
45
|
end
|
53
|
-
|
54
|
-
def
|
55
|
-
|
46
|
+
|
47
|
+
def container
|
48
|
+
"bottles"
|
56
49
|
end
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
def name
|
61
|
-
self.zero? ? 'no more' : to_i
|
50
|
+
|
51
|
+
def action
|
52
|
+
"Take #{pronoun} down and pass it around"
|
62
53
|
end
|
63
|
-
|
54
|
+
|
64
55
|
def pronoun
|
65
|
-
|
66
|
-
end
|
67
|
-
|
68
|
-
def container
|
69
|
-
self == 1 ? 'bottle' : 'bottles'
|
56
|
+
"one"
|
70
57
|
end
|
71
|
-
|
72
|
-
def
|
73
|
-
|
58
|
+
|
59
|
+
def successor
|
60
|
+
context.bottle_role_player(pred)
|
74
61
|
end
|
75
62
|
end
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
def inventory
|
83
|
-
%{#{number.name} #{number.container} of beer}
|
84
|
-
end
|
85
|
-
|
86
|
-
def status
|
87
|
-
%{#{inventory} #{placement} #{name}}
|
88
|
-
end
|
89
|
-
|
90
|
-
def subtraction
|
91
|
-
%{take #{number.pronoun} #{removal_strategy}}
|
92
|
-
end
|
63
|
+
|
64
|
+
# Inherit from existing role
|
65
|
+
def self.bottle_role(name, &block)
|
66
|
+
mod_name = RoleName(name)
|
67
|
+
klass = Class.new(BottleNumber, &block)
|
68
|
+
const_set(mod_name, klass)
|
93
69
|
end
|
94
|
-
end
|
95
70
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
def placement
|
100
|
-
'on'
|
71
|
+
def self.register_bottle_role(number, klass)
|
72
|
+
@@registry ||= Hash.new { BottleNumber }
|
73
|
+
@@registry[number] = klass
|
101
74
|
end
|
102
|
-
|
103
|
-
def
|
104
|
-
|
75
|
+
|
76
|
+
def bottle_role_for(number)
|
77
|
+
@@registry[number]
|
105
78
|
end
|
106
|
-
|
107
|
-
def
|
108
|
-
|
79
|
+
|
80
|
+
def map_role_bottle_number(num)
|
81
|
+
map_role(:bottle_number, bottle_role_for(num), num)
|
109
82
|
end
|
110
|
-
end
|
111
83
|
|
112
|
-
|
113
|
-
|
114
|
-
'down'
|
84
|
+
def bottle_role_player(number)
|
85
|
+
bottle_role_for(number).new(number)
|
115
86
|
end
|
116
|
-
end
|
117
87
|
|
118
|
-
|
119
|
-
|
120
|
-
|
88
|
+
role :bottle_number_0, :bottle_role do
|
89
|
+
handles 0
|
90
|
+
|
91
|
+
def quantity
|
92
|
+
"no more"
|
93
|
+
end
|
94
|
+
|
95
|
+
def action
|
96
|
+
"Go to the store and buy some more"
|
97
|
+
end
|
98
|
+
|
99
|
+
def successor
|
100
|
+
context.bottle_role_player(99)
|
101
|
+
end
|
121
102
|
end
|
122
|
-
|
123
|
-
|
124
|
-
|
103
|
+
|
104
|
+
role :bottle_number_1, :bottle_role do
|
105
|
+
handles 1
|
106
|
+
|
107
|
+
def container
|
108
|
+
"bottle"
|
109
|
+
end
|
110
|
+
|
111
|
+
def pronoun
|
112
|
+
"it"
|
113
|
+
end
|
125
114
|
end
|
126
115
|
end
|
127
116
|
|
128
|
-
class
|
129
|
-
|
130
|
-
|
131
|
-
STDOUT.puts(value)
|
117
|
+
class Bottles
|
118
|
+
def song_template(upper: 99, lower: 0)
|
119
|
+
CountdownSong.new(max: upper, min: lower)
|
132
120
|
end
|
133
|
-
end
|
134
121
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
122
|
+
def song
|
123
|
+
song_template.sing_with(BottleVerse)
|
124
|
+
end
|
125
|
+
|
126
|
+
def verses(upper, lower)
|
127
|
+
song_template(upper: upper, lower: lower).sing_with(BottleVerse)
|
128
|
+
end
|
129
|
+
|
130
|
+
def verse(number)
|
131
|
+
song_template.verse(number, BottleVerse)
|
141
132
|
end
|
142
133
|
end
|
143
134
|
|
144
|
-
|
145
|
-
# Countdown.new(Sheet.new, 3, Box.new).start
|
135
|
+
puts Bottles.new.song
|
@@ -33,12 +33,10 @@ module Surrounded
|
|
33
33
|
mod = Module.new
|
34
34
|
mod.class_eval {
|
35
35
|
define_method "disallow_#{name}?" do
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
remove_behaviors
|
41
|
-
end
|
36
|
+
apply_behaviors
|
37
|
+
instance_exec(&block)
|
38
|
+
ensure
|
39
|
+
remove_behaviors
|
42
40
|
end
|
43
41
|
}
|
44
42
|
const_set("SurroundedAccess#{name}", mod)
|
@@ -55,22 +53,16 @@ module Surrounded
|
|
55
53
|
# in disallow blocks.
|
56
54
|
def triggers
|
57
55
|
all_triggers.select {|name|
|
58
|
-
|
59
|
-
!self.respond_to?(method_restrictor, true) || !self.send(method_restrictor)
|
56
|
+
allow?(name)
|
60
57
|
}.to_set
|
61
58
|
end
|
62
59
|
|
63
60
|
# Ask if the context will allow access to a trigger given the current players.
|
64
61
|
def allow?(name)
|
65
|
-
unless self.respond_to?(name)
|
66
|
-
|
67
|
-
|
68
|
-
if self.respond_to?("disallow_#{name}?")
|
69
|
-
!self.public_send("disallow_#{name}?")
|
70
|
-
else
|
71
|
-
true
|
72
|
-
end
|
62
|
+
raise NoMethodError, %{undefined method `#{name}' for #{self.inspect}} unless self.respond_to?(name)
|
63
|
+
predicate = "disallow_#{name}?"
|
64
|
+
!self.respond_to?(predicate) || !self.public_send(predicate)
|
73
65
|
end
|
74
66
|
end
|
75
67
|
end
|
76
|
-
end
|
68
|
+
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
module Surrounded
|
2
2
|
module Context
|
3
3
|
module Initializing
|
4
|
+
extend Seclusion
|
4
5
|
# Shorthand for creating an instance level initialize method which
|
5
6
|
# handles the mapping of the given arguments to their named role.
|
6
7
|
def initialize_without_keywords(*setup_args, &block)
|
@@ -41,4 +42,4 @@ module Surrounded
|
|
41
42
|
end
|
42
43
|
end
|
43
44
|
end
|
44
|
-
end
|
45
|
+
end
|
@@ -15,8 +15,9 @@ module Surrounded
|
|
15
15
|
# circumvent method_missing
|
16
16
|
mod.instance_methods(false).each do |meth|
|
17
17
|
num = __LINE__; klass.class_eval %{
|
18
|
-
def #{meth}(
|
19
|
-
__behaviors__.instance_method(:#{meth}).bind(@object)
|
18
|
+
def #{meth}(...)
|
19
|
+
@#{meth}_method ||= __behaviors__.instance_method(:#{meth}).bind(@object)
|
20
|
+
@#{meth}_method.call(...)
|
20
21
|
end
|
21
22
|
}, __FILE__, num
|
22
23
|
end
|
@@ -57,8 +58,8 @@ module Surrounded
|
|
57
58
|
@object = object
|
58
59
|
end
|
59
60
|
|
60
|
-
def method_missing(meth,
|
61
|
-
@object.send(meth,
|
61
|
+
def method_missing(meth, ...)
|
62
|
+
@object.send(meth, ...)
|
62
63
|
end
|
63
64
|
|
64
65
|
def respond_to_missing?(meth, include_private=false)
|
@@ -66,4 +67,4 @@ module Surrounded
|
|
66
67
|
end
|
67
68
|
end
|
68
69
|
end
|
69
|
-
end
|
70
|
+
end
|
@@ -6,12 +6,16 @@ module Surrounded
|
|
6
6
|
extend Forwardable
|
7
7
|
|
8
8
|
class << self
|
9
|
+
# Get the role map container and provide an alternative if desired
|
10
|
+
# Ex: RoleMap.from_base(SomeCustomContainer)
|
9
11
|
def from_base(klass=::Triad)
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
12
|
+
unless const_defined?(:Container)
|
13
|
+
role_mapper = Class.new(self)
|
14
|
+
role_mapper.container_class=(klass)
|
15
|
+
Surrounded::Exceptions.define(role_mapper, exceptions: :ItemNotPresent, namespace: klass)
|
16
|
+
const_set(:Container, role_mapper)
|
17
|
+
end
|
18
|
+
const_get(:Container)
|
15
19
|
end
|
16
20
|
|
17
21
|
def container_class=(klass)
|
@@ -19,23 +23,28 @@ module Surrounded
|
|
19
23
|
end
|
20
24
|
end
|
21
25
|
|
26
|
+
def_delegators :container, :update, :each, :values, :keys
|
27
|
+
|
22
28
|
def container
|
23
29
|
@container ||= self.class.instance_variable_get(:@container_class).new
|
24
30
|
end
|
25
31
|
|
32
|
+
# Check if a role exists in the map
|
26
33
|
def role?(role)
|
27
34
|
keys.include?(role)
|
28
35
|
end
|
29
36
|
|
37
|
+
# Check if an object is playing a role in this map
|
30
38
|
def role_player?(object)
|
31
39
|
!values(object).empty?
|
32
|
-
rescue ::
|
40
|
+
rescue self.container.class::ItemNotPresent
|
33
41
|
false
|
34
42
|
end
|
35
43
|
|
44
|
+
# Get the object playing the given role
|
36
45
|
def assigned_player(role)
|
37
46
|
values(role).first
|
38
47
|
end
|
39
48
|
end
|
40
49
|
end
|
41
|
-
end
|
50
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Surrounded
|
2
|
+
module Context
|
3
|
+
module Seclusion
|
4
|
+
# Set a named constant and make it private
|
5
|
+
def private_const_set(name, const)
|
6
|
+
unless role_const_defined?(name)
|
7
|
+
const = const_set(name, const)
|
8
|
+
private_constant name.to_sym
|
9
|
+
end
|
10
|
+
const
|
11
|
+
end
|
12
|
+
|
13
|
+
# Create attr_reader for the named methods and make them private
|
14
|
+
def private_attr_reader(*method_names)
|
15
|
+
attr_reader(*method_names)
|
16
|
+
private(*method_names)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -54,7 +54,7 @@ module Surrounded
|
|
54
54
|
|
55
55
|
def define_trigger(name)
|
56
56
|
line = __LINE__; self.class_eval %{
|
57
|
-
def #{name}(
|
57
|
+
def #{name}(...)
|
58
58
|
begin
|
59
59
|
apply_behaviors
|
60
60
|
|
@@ -71,7 +71,7 @@ module Surrounded
|
|
71
71
|
if method_defined?(name)
|
72
72
|
%{super}
|
73
73
|
else
|
74
|
-
%{self.send("__trigger_#{name}",
|
74
|
+
%{self.send("__trigger_#{name}", ...)}
|
75
75
|
end
|
76
76
|
end
|
77
77
|
|
@@ -85,4 +85,4 @@ module Surrounded
|
|
85
85
|
end
|
86
86
|
end
|
87
87
|
end
|
88
|
-
end
|
88
|
+
end
|
data/lib/surrounded/context.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'set'
|
2
2
|
require 'surrounded/exceptions'
|
3
3
|
require 'surrounded/context/role_map'
|
4
|
+
require 'surrounded/context/seclusion'
|
4
5
|
require 'surrounded/context/role_builders'
|
5
6
|
require 'surrounded/context/initializing'
|
6
7
|
require 'surrounded/context/forwarding'
|
@@ -20,7 +21,7 @@ module Surrounded
|
|
20
21
|
module Context
|
21
22
|
def self.extended(base)
|
22
23
|
base.class_eval {
|
23
|
-
extend RoleBuilders, Initializing, Forwarding, NameCollisionDetector
|
24
|
+
extend Seclusion, RoleBuilders, Initializing, Forwarding, NameCollisionDetector
|
24
25
|
|
25
26
|
@triggers = Set.new
|
26
27
|
include InstanceMethods
|
@@ -69,21 +70,6 @@ module Surrounded
|
|
69
70
|
const_defined?(name, false)
|
70
71
|
end
|
71
72
|
|
72
|
-
# Set a named constant and make it private
|
73
|
-
def private_const_set(name, const)
|
74
|
-
unless role_const_defined?(name)
|
75
|
-
const = const_set(name, const)
|
76
|
-
private_constant name.to_sym
|
77
|
-
end
|
78
|
-
const
|
79
|
-
end
|
80
|
-
|
81
|
-
# Create attr_reader for the named methods and make them private
|
82
|
-
def private_attr_reader(*method_names)
|
83
|
-
attr_reader(*method_names)
|
84
|
-
private(*method_names)
|
85
|
-
end
|
86
|
-
|
87
73
|
# Conditional const_get for a named role behavior
|
88
74
|
def role_const(name)
|
89
75
|
if role_const_defined?(name)
|
@@ -119,10 +105,11 @@ module Surrounded
|
|
119
105
|
self.class.triggers
|
120
106
|
end
|
121
107
|
|
108
|
+
# Reuse the same context object but pass new values
|
122
109
|
def rebind(**options_hash)
|
123
110
|
clear_instance_variables
|
124
111
|
begin
|
125
|
-
initialize(options_hash)
|
112
|
+
initialize(**options_hash)
|
126
113
|
rescue ArgumentError
|
127
114
|
initialize(*options_hash.values)
|
128
115
|
end
|
data/lib/surrounded/shortcuts.rb
CHANGED
@@ -3,9 +3,21 @@ module Surrounded
|
|
3
3
|
private
|
4
4
|
|
5
5
|
def define_shortcut(name)
|
6
|
-
|
7
|
-
|
8
|
-
|
6
|
+
# if keyword initialize
|
7
|
+
if instance_method(:initialize).parameters.dig(0,0) == :keyreq
|
8
|
+
singleton_class.send(:define_method, name) do |**args|
|
9
|
+
instance = begin
|
10
|
+
self.new(**args)
|
11
|
+
end
|
12
|
+
instance.public_send(name)
|
13
|
+
end
|
14
|
+
else # non-keyword initialize
|
15
|
+
singleton_class.send(:define_method, name) do |*args|
|
16
|
+
instance = begin
|
17
|
+
self.new(*args)
|
18
|
+
end
|
19
|
+
instance.public_send(name)
|
20
|
+
end
|
9
21
|
end
|
10
22
|
end
|
11
23
|
|
@@ -16,4 +28,4 @@ module Surrounded
|
|
16
28
|
super
|
17
29
|
end
|
18
30
|
end
|
19
|
-
end
|
31
|
+
end
|
data/lib/surrounded/version.rb
CHANGED
data/surrounded.gemspec
CHANGED
@@ -17,10 +17,35 @@ class ShortcutContext
|
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
|
+
class ShortcutContextNoKeywords
|
21
|
+
extend Surrounded::Context
|
22
|
+
shortcut_triggers
|
23
|
+
|
24
|
+
initialize_without_keywords :user, :other
|
25
|
+
|
26
|
+
trigger :shorty do
|
27
|
+
user.speak
|
28
|
+
end
|
29
|
+
|
30
|
+
role :user do
|
31
|
+
def speak
|
32
|
+
'it works, shorty!'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
20
37
|
describe Surrounded::Context, 'shortcuts' do
|
21
38
|
let(:user){ User.new("Jim") }
|
22
39
|
let(:other){ User.new("Guille") }
|
23
40
|
it 'creates shortcut class methods for triggers' do
|
24
41
|
assert_equal 'it works, shorty!', ShortcutContext.shorty(user: user, other: other)
|
25
42
|
end
|
26
|
-
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe Surrounded::Context, 'shortcuts with initialize_without_keywords' do
|
46
|
+
let(:user){ User.new("Jim") }
|
47
|
+
let(:other){ User.new("Guille") }
|
48
|
+
it 'creates shortcut class methods for triggers' do
|
49
|
+
assert_equal 'it works, shorty!', ShortcutContextNoKeywords.shorty(user, other)
|
50
|
+
end
|
51
|
+
end
|
data/test/example_proxy_test.rb
CHANGED
@@ -77,7 +77,7 @@ describe ProxyContext do
|
|
77
77
|
end
|
78
78
|
|
79
79
|
it 'passes missing methods up the ancestry of the object' do
|
80
|
-
err =
|
80
|
+
err = _{ context.admin_missing_method }.must_raise(NoMethodError)
|
81
81
|
|
82
82
|
assert_match(/ProxyUser.*name="Jim"/, err.message)
|
83
83
|
end
|
data/test/initialization_test.rb
CHANGED
@@ -28,7 +28,7 @@ describe Surrounded::Context, '.initialize' do
|
|
28
28
|
err = assert_raises(ArgumentError){
|
29
29
|
KeywordContext.new(other_user: User.new('Amy'))
|
30
30
|
}
|
31
|
-
assert_match(/missing keyword: user/, err.message)
|
31
|
+
assert_match(/missing keyword: :?user/, err.message)
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
@@ -53,11 +53,35 @@ describe Surrounded::Context, '.role' do
|
|
53
53
|
def hello
|
54
54
|
'hello from admin'
|
55
55
|
end
|
56
|
+
|
57
|
+
def splat_args(*args)
|
58
|
+
args
|
59
|
+
end
|
60
|
+
|
61
|
+
def keyword_args(**kwargs)
|
62
|
+
kwargs
|
63
|
+
end
|
64
|
+
|
65
|
+
def mixed_args(*args, **kwargs)
|
66
|
+
[args, kwargs]
|
67
|
+
end
|
56
68
|
end
|
57
69
|
|
58
70
|
trigger :admin_hello do
|
59
71
|
admin.hello
|
60
72
|
end
|
73
|
+
|
74
|
+
trigger :splat_args do |*args|
|
75
|
+
admin.splat_args(*args)
|
76
|
+
end
|
77
|
+
|
78
|
+
trigger :keyword_args do |**kwargs|
|
79
|
+
admin.keyword_args(**kwargs)
|
80
|
+
end
|
81
|
+
|
82
|
+
trigger :mixed_args do |*args, **kwargs|
|
83
|
+
admin.mixed_args(*args, **kwargs)
|
84
|
+
end
|
61
85
|
end
|
62
86
|
|
63
87
|
class Hello
|
@@ -85,6 +109,18 @@ describe Surrounded::Context, '.role' do
|
|
85
109
|
it 'creates a private accessor method' do
|
86
110
|
assert context.respond_to?(:admin, true)
|
87
111
|
end
|
112
|
+
|
113
|
+
it 'works with multiple args' do
|
114
|
+
assert_equal context.splat_args("one", "two"), %w[ one two ]
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'works with multiple keyword args' do
|
118
|
+
assert_equal context.keyword_args(one: "one", two: "two"), { one: "one", two: "two" }
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'works with multiple mixed args' do
|
122
|
+
assert_equal context.mixed_args("one", :two, three: "three", four: "four"), [["one", :two], { three: "three", four: "four" }]
|
123
|
+
end
|
88
124
|
end
|
89
125
|
|
90
126
|
describe 'unknown' do
|
@@ -149,4 +185,4 @@ describe Surrounded::Context, '.role' do
|
|
149
185
|
end
|
150
186
|
end
|
151
187
|
end
|
152
|
-
end
|
188
|
+
end
|
data/test/test_helper.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: surrounded
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- "'Jim Gay'"
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-11-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: triad
|
@@ -24,20 +24,6 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: 0.3.0
|
27
|
-
- !ruby/object:Gem::Dependency
|
28
|
-
name: bundler
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - "~>"
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: '1.12'
|
34
|
-
type: :development
|
35
|
-
prerelease: false
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - "~>"
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: '1.12'
|
41
27
|
- !ruby/object:Gem::Dependency
|
42
28
|
name: rake
|
43
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -60,10 +46,11 @@ extensions: []
|
|
60
46
|
extra_rdoc_files: []
|
61
47
|
files:
|
62
48
|
- ".codeclimate.yml"
|
49
|
+
- ".github/workflows/codeql-analysis.yml"
|
50
|
+
- ".github/workflows/test.yml"
|
63
51
|
- ".gitignore"
|
64
52
|
- ".pullreview.yml"
|
65
53
|
- ".simplecov"
|
66
|
-
- ".travis.yml"
|
67
54
|
- Changelog.md
|
68
55
|
- Gemfile
|
69
56
|
- LICENSE.txt
|
@@ -80,6 +67,7 @@ files:
|
|
80
67
|
- lib/surrounded/context/negotiator.rb
|
81
68
|
- lib/surrounded/context/role_builders.rb
|
82
69
|
- lib/surrounded/context/role_map.rb
|
70
|
+
- lib/surrounded/context/seclusion.rb
|
83
71
|
- lib/surrounded/context/trigger_controls.rb
|
84
72
|
- lib/surrounded/east_oriented.rb
|
85
73
|
- lib/surrounded/exceptions.rb
|
@@ -109,7 +97,7 @@ homepage: http://github.com/saturnflyer/surrounded
|
|
109
97
|
licenses:
|
110
98
|
- MIT
|
111
99
|
metadata: {}
|
112
|
-
post_install_message:
|
100
|
+
post_install_message:
|
113
101
|
rdoc_options: []
|
114
102
|
require_paths:
|
115
103
|
- lib
|
@@ -124,9 +112,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
124
112
|
- !ruby/object:Gem::Version
|
125
113
|
version: '0'
|
126
114
|
requirements: []
|
127
|
-
|
128
|
-
|
129
|
-
signing_key:
|
115
|
+
rubygems_version: 3.3.23
|
116
|
+
signing_key:
|
130
117
|
specification_version: 4
|
131
118
|
summary: Create encapsulated environments for your objects.
|
132
119
|
test_files:
|
data/.travis.yml
DELETED
@@ -1,19 +0,0 @@
|
|
1
|
-
before_install:
|
2
|
-
- gem install bundler
|
3
|
-
language: ruby
|
4
|
-
cache: bundler
|
5
|
-
rvm:
|
6
|
-
- 2.4.1
|
7
|
-
- 2.3.4
|
8
|
-
- 2.2.7
|
9
|
-
- ruby-head
|
10
|
-
- jruby-head
|
11
|
-
matrix:
|
12
|
-
allow_failures:
|
13
|
-
- rvm: ruby-head
|
14
|
-
- rvm: jruby-head
|
15
|
-
addons:
|
16
|
-
code_climate:
|
17
|
-
repo_token: 7488b157e7b7f48eac865a9f830fe90a39e6ac10b17f854e17b9529e1854762c
|
18
|
-
after_success:
|
19
|
-
- bundle exec codeclimate-test-reporter
|