admission 0.1.9 → 0.2.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: f6fc32c93d1bd50bfe3cea39b1e7e62be2add21e
4
- data.tar.gz: '095f27c6a9e95f503954ff5bea97ae12c828ef8f'
3
+ metadata.gz: 4eb5d14f7cda027b322111bf9f1e065063510222
4
+ data.tar.gz: 80dabfdd53d16c0300a53d2aea8aee8e93e5e2c7
5
5
  SHA512:
6
- metadata.gz: e46fdce9e3feea2f4c99c7d38a9ef606499f19fac82491644c82625da439b0337392d04e731e6ce2e9efd6f6ae97ddadef5c9f267df8d643432d2fccb8827c95
7
- data.tar.gz: daefd3683677ec4ff98f1c8a5bb2147baf44d8690423024954a78859aa45fdbce73a26ae81ee9ad16b297b1127a3005202efa927748fd49501f345f3c10cd939
6
+ metadata.gz: 35eed6cd8fcc1219ef8e9073f1b790ecdea387550c8abfb158777e9c8afea4fde0340e08729b1460c366fea9c2651287b6b98f2555b2c884dcec63dd73ac3aab
7
+ data.tar.gz: bd0badfc8814b1dadaa883188ddfc2bfb110a9727b552140c3a54803577f6639cd08f58bb78b0713eda310617c5328075e34ecfd072bdad78391baafc4bc76b3
data/.gitignore CHANGED
@@ -34,4 +34,7 @@ Gemfile.lock
34
34
 
35
35
  ## Specific for InteliJ:
36
36
  /.idea
37
- *.iml
37
+ *.iml
38
+
39
+ /visualisation/node_modules
40
+ /visualisation/build
data/Gemfile CHANGED
@@ -3,4 +3,8 @@ source "https://rubygems.org"
3
3
 
4
4
  gem 'rspec'
5
5
  # gem 'mutant-rspec'
6
- gem 'byebug'
6
+ gem 'byebug'
7
+
8
+ gem 'sinatra', '~> 2.0'
9
+ gem 'sinatra-contrib'
10
+ gem 'haml'
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ # Set up gems listed in the Gemfile.
5
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
6
+ require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
7
+
8
+ require 'rack'
9
+ require 'byebug'
10
+
11
+ require_relative '../lib/admission'
12
+ require_relative '../lib/admission/visualisation'
13
+ require_relative '../spec/test_context/index'
14
+
15
+ Admission::Visualisation.set :js_entry,
16
+ Admission::Visualisation::ASSETS_PATH.join('build', 'admission_visualisation.js')
17
+
18
+ Admission::Visualisation.set :admission_data,
19
+ {
20
+ order: PRIVILEGES_ORDER,
21
+ rules: ACTIONS_RULES,
22
+ arbitrator: Admission::Arbitration
23
+ }
24
+
25
+
26
+ Rack::Handler::WEBrick.run Admission::Visualisation
@@ -5,9 +5,11 @@ class Admission::Arbitration
5
5
  @person = person
6
6
  @rules_index = rules_index
7
7
  @request = request.to_sym
8
+ @decisions = {}
8
9
  end
9
10
 
10
11
  def prepare_sitting context=nil
12
+ return if context == @context
11
13
  @context = context
12
14
  @decisions = {}
13
15
  end
@@ -30,6 +30,12 @@ class Admission::Privilege
30
30
  hash == other.hash
31
31
  end
32
32
 
33
+ def eql_or_inherits? sought
34
+ return true if eql? sought
35
+ return false unless inherited
36
+ inherited.any?{|pi| pi.eql_or_inherits? sought}
37
+ end
38
+
33
39
  def text_key
34
40
  level == BASE_LEVEL_NAME ? name.to_s : "#{name}-#{level}"
35
41
  end
@@ -5,6 +5,7 @@ class Admission::ResourceArbitration < Admission::Arbitration
5
5
  scope, @resource = scope_and_resource scope_or_resource
6
6
  @rules_index = rules_index[scope] || {}
7
7
  @request = request.to_sym
8
+ @decisions = {}
8
9
  end
9
10
 
10
11
  def make_decision from_rules, privilege
@@ -4,13 +4,25 @@ class Admission::Status
4
4
 
5
5
  def initialize person, privileges, rules, arbiter
6
6
  @person = person
7
- @privileges = (privileges.nil? || privileges.empty?) ? nil : privileges
8
7
  @rules = rules
9
8
  @arbiter = arbiter
9
+
10
+ @privileges = if privileges.nil? || privileges.empty?
11
+ nil
12
+
13
+ else
14
+ grouped = privileges.inject Hash.new do |h, p|
15
+ hash = p.context.hash rescue nil.hash
16
+ (h[hash] ||= []) << p
17
+ h
18
+ end
19
+
20
+ grouped.values.flatten.freeze
21
+ end
10
22
  end
11
23
 
12
24
  def can? *args
13
- return false unless @privileges
25
+ return false unless privileges
14
26
  process_request @arbiter.new(person, rules, *args)
15
27
  end
16
28
 
@@ -26,11 +38,18 @@ class Admission::Status
26
38
  end
27
39
  end
28
40
 
41
+ def has? sought
42
+ return false unless privileges
43
+
44
+ list = privilege.context ? privileges.select{|p| p.context == privilege.context} : privileges
45
+ list.any?{|p| p.eql_or_inherits? sought}
46
+ end
47
+
29
48
  def allowed_in_contexts *args
30
- return [] unless @privileges
49
+ return [] unless privileges
31
50
  arbitration = @arbiter.new person, rules, *args
32
51
 
33
- @privileges.reduce [] do |list, privilege|
52
+ privileges.reduce [] do |list, privilege|
34
53
  context = privilege.context
35
54
 
36
55
  unless list.include? context
@@ -1,3 +1,3 @@
1
1
  module Admission
2
- VERSION = '0.1.9'
2
+ VERSION = '0.2.0'
3
3
  end
@@ -0,0 +1,81 @@
1
+ require 'sinatra/base'
2
+ require 'sinatra/json'
3
+ require 'haml'
4
+ require 'pathname'
5
+
6
+ class Admission::Visualisation < Sinatra::Base
7
+ ASSETS_PATH = Pathname.new(__FILE__).join('..', '..', '..', 'visualisation')
8
+
9
+ enable :inline_templates
10
+
11
+ set :js_entry, ASSETS_PATH.join('dist', 'admission_visualisation.js')
12
+
13
+ get '/' do
14
+ haml :index
15
+ end
16
+
17
+ get '/admission_visualisation.js' do
18
+ send_file settings.js_entry
19
+ end
20
+
21
+ get '/admission_data' do
22
+ json Admission::Visualisation.admission_data_to_js(
23
+ **settings.admission_data)
24
+ end
25
+
26
+ def self.admission_data_to_js order:, rules:, arbitrator:, **_
27
+ js_data = {}
28
+
29
+ top_levels = []
30
+ privileges = order.values.inject Array.new do |arr, levels|
31
+ tops, others = levels.to_a.partition{|key, _| key == :'^'}
32
+ tops.first[1].tap{|privilege| top_levels << privilege.text_key}
33
+
34
+ others.each do |_, privilege|
35
+ arr << {name: privilege.name, level: privilege.level,
36
+ inherits: privilege.inherited && privilege.inherited.map(&:text_key)}
37
+ end
38
+
39
+ arr
40
+ end
41
+ js_data[:top_levels] = top_levels
42
+ js_data[:privileges] = privileges
43
+
44
+ rules = if arbitrator == Admission::Arbitration
45
+ rules.to_a.map do |scope, index|
46
+
47
+ index = index.to_a.map do |privilege, rule|
48
+ if rule.is_a? Proc
49
+ rule = 'proc'
50
+ end
51
+
52
+ [privilege.text_key, rule]
53
+ end
54
+
55
+ [scope, Hash[index]]
56
+ end
57
+
58
+ else
59
+ raise "not implemented for #{arbitrator.name}"
60
+
61
+ end
62
+ js_data[:rules] = Hash[rules]
63
+
64
+ js_data
65
+ end
66
+
67
+ end
68
+
69
+ __END__
70
+
71
+ @@ layout
72
+ !!! 5
73
+ %html
74
+ %head
75
+ %title= 'Admission'
76
+ %body.flex-column
77
+ = yield
78
+
79
+ @@ index
80
+ #admission-visualisation(data-url="/admission_data")
81
+ %script(src="/admission_visualisation.js")
@@ -22,7 +22,6 @@ RSpec.describe 'resources_arbitrating' do
22
22
  end
23
23
 
24
24
  def rule scope, action, privilege
25
- byebug if $bug
26
25
  arbitration(scope, action, privilege.context).rule_per_privilege privilege
27
26
  end
28
27
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  PRIVILEGES_ORDER = Admission::Privilege.define_order do
4
4
  privilege :vassal, levels: %i[lord]
5
- privilege :human, levels: %i[count king]
5
+ privilege :human, levels: %i[count king]
6
6
  privilege :emperor, inherits: %i[vassal human]
7
7
  end
8
8
 
@@ -68,6 +68,33 @@ RSpec.describe Admission::Privilege do
68
68
  end
69
69
 
70
70
 
71
+ describe '#eql_or_inherits?' do
72
+
73
+ it 'returns true since it equals sought privilege' do
74
+ expect(privilege.eql_or_inherits? privilege).to eq(true)
75
+ end
76
+
77
+ it 'return false when does not inherit any' do
78
+ expect(privilege_superman.eql_or_inherits? privilege_uberman).to eq(false)
79
+ end
80
+
81
+ it 'finds nested inherited privilege and therefore evaluates to true' do
82
+ top_privilege = new_privilege 'man', 'top'
83
+ top_privilege.inherits_from new_privilege('man', 'branch'),
84
+ new_privilege('man', 'middle').tap{|p| p.inherits_from new_privilege('man')}
85
+
86
+ sought = new_privilege 'man'
87
+ expect(top_privilege.eql_or_inherits? sought).to eq(true)
88
+ expect(top_privilege.eql_or_inherits? new_privilege('man', 'nope')).to eq(false)
89
+ end
90
+
91
+ it 'ignores context' do
92
+ expect(privilege.eql_or_inherits? privilege.dup_with_context(:czech)).to eq(true)
93
+ end
94
+
95
+ end
96
+
97
+
71
98
  describe '#to_s' do
72
99
 
73
100
  it 'prints name and level' do
@@ -27,14 +27,32 @@ RSpec.describe Admission::Status do
27
27
  )
28
28
  end
29
29
 
30
- it 'sets privileges' do
31
- instance = Admission::Status.new :person, ['kkk'], :rules, :arbiter
30
+ it 'sets privileges and freezes them' do
31
+ instance = Admission::Status.new :person, [:czech], :rules, :arbiter
32
32
  expect(instance).to have_inst_vars(
33
33
  person: :person,
34
- privileges: ['kkk'],
34
+ privileges: [:czech],
35
35
  rules: :rules,
36
36
  arbiter: :arbiter
37
37
  )
38
+ expect(instance.privileges).to be_frozen
39
+ end
40
+
41
+ it 'sorts privileges by context' do
42
+ instance = Admission::Status.new :person, [
43
+ privilege(nil),
44
+ privilege(:czech),
45
+ privilege(15),
46
+ privilege(:czech),
47
+ privilege({a: 15}),
48
+ privilege({a: {f: 1}}),
49
+ privilege(nil),
50
+ privilege({a: 15}),
51
+ ], :rules, :arbiter
52
+ expect(instance.privileges.map(&:context)).to eq([
53
+ nil, nil, :czech, :czech, 15, {:a=>15}, {:a=>15}, {:a=>{:f=>1}}
54
+ ])
55
+ expect(instance.privileges).to be_frozen
38
56
  end
39
57
 
40
58
  end
@@ -0,0 +1,7 @@
1
+ {
2
+ // "sourceMaps": true,
3
+ "presets": ["es2015"],
4
+ "plugins": [
5
+ ["transform-react-jsx", { "pragma": "preact.h" }]
6
+ ]
7
+ }
File without changes
@@ -0,0 +1,73 @@
1
+ import preact from 'preact';
2
+ import PrivilegesPanel from './privileges_panel';
3
+ import classnames from 'classnames';
4
+
5
+ export default class AppContainer extends preact.Component {
6
+
7
+ constructor (props) {
8
+ super(props);
9
+
10
+ this.switchToPrivileges = this.changePanel.bind(this, 'privileges');
11
+ this.switchToRules = this.changePanel.bind(this, 'rules');
12
+ }
13
+
14
+ render ({app}, {loaded, load_fail, panel}) {
15
+ if (!loaded) return <div className="splash-message">
16
+ <code>... loading admission data ...</code>
17
+ </div>;
18
+
19
+ if (load_fail) return <div className="splash-message">
20
+ <h4>failed to load admission data</h4>
21
+ <code>{load_fail}</code>
22
+ </div>;
23
+
24
+ return <div>
25
+ <ul className="panels-list">
26
+
27
+ <li
28
+ onClick={this.switchToPrivileges}
29
+ className={classnames({'active': panel === 'privileges'})}>
30
+ Privileges Order
31
+ </li>
32
+
33
+ <li
34
+ onClick={this.switchToRules}
35
+ className={classnames({'active': panel === 'rules'})}>
36
+ Rules Listing
37
+ </li>
38
+
39
+ </ul>
40
+
41
+ {this.renderPanel()}
42
+ </div>;
43
+ }
44
+
45
+ componentDidMount () {
46
+ const store = this.props.app.store;
47
+
48
+ this.store_unsibscribe = store.subscribe(() => {
49
+ const state = store.getState();
50
+ this.setState({loaded: state.loaded, panel: state.panel});
51
+ });
52
+
53
+ setTimeout(this.props.onMounted, 0);
54
+ }
55
+
56
+ componentWillUnmount () {
57
+ this.store_unsibscribe();
58
+ }
59
+
60
+ changePanel(panel) {
61
+ this.props.app.store.dispatch({type: 'PANEL_CHANGE', panel: panel});
62
+ }
63
+
64
+ renderPanel () {
65
+ const app = this.props.app;
66
+ switch (this.state.panel) {
67
+ case 'privileges':
68
+ return <PrivilegesPanel app={app} />;
69
+ break;
70
+
71
+ }
72
+ }
73
+ }
@@ -0,0 +1,164 @@
1
+ import preact from 'preact';
2
+ import classnames from 'classnames';
3
+
4
+ export default class InputWithSelect extends preact.Component {
5
+
6
+ constructor (props) {
7
+ super(props);
8
+
9
+ this.state = {
10
+ text: props.defaultText || '',
11
+ matching: null,
12
+ };
13
+
14
+ this.setParentRef = ref => this.element = ref;
15
+ this.setListRef = ref => this.list = ref;
16
+ this.onKeyDown = props.app.debounce(this.onKeyDown.bind(this), 200);
17
+ this.toggleList = props.app.debounce(this.toggleList.bind(this), 400, true);
18
+ this.closeList = this.closeList.bind(this);
19
+ this.onSelected = this.onSelected.bind(this);
20
+ }
21
+
22
+ render ({app, placeholder}, {text, matching}) {
23
+ if (!matching) this.list = null;
24
+
25
+ return <div
26
+ ref={this.setParentRef}
27
+ className="controls-select">
28
+
29
+ <div className="_inputs">
30
+ <input
31
+ type="text"
32
+ className="input_text"
33
+ placeholder={placeholder}
34
+ onKeyDown={this.onKeyDown}
35
+ value={text}/>
36
+
37
+ <button
38
+ tabIndex="-1"
39
+ className="button"
40
+ onClick={this.toggleList}>
41
+ &#8964;
42
+ </button>
43
+ </div>
44
+
45
+ {matching && <DropdownList
46
+ ref={this.setListRef}
47
+ app={app}
48
+ items={matching}
49
+ toSelect={this.onSelected}
50
+ toClose={this.closeList}
51
+ />}
52
+
53
+ </div>;
54
+ }
55
+
56
+ componentDidMount () {
57
+ this._outside_click_listener = e => {
58
+ if (!this.element.contains(e.target) && this.list) {
59
+ this.closeList();
60
+ }
61
+ };
62
+ document.addEventListener('click', this._outside_click_listener);
63
+ }
64
+
65
+ componentWillUnmount () {
66
+ document.removeEventListener('click', this._outside_click_listener);
67
+ }
68
+
69
+ componentWillReceiveProps (new_props) {
70
+ if (new_props.defaultText !== this.props.defaultText) this.setState({text: new_props.defaultText});
71
+ }
72
+
73
+ onKeyDown (e) {
74
+ if (this.list && this.list.onKeyDown(e)) return;
75
+
76
+ const text = e.target.value.trim();
77
+ let matching = null;
78
+ if ((text && text !==this.state.text) || e.keyCode === 40) {
79
+ matching = filter_items(this.props.all_items, text);
80
+ }
81
+ this.setState({matching, text});
82
+ }
83
+
84
+ toggleList () {
85
+ if (this.list) {
86
+ this.closeList();
87
+
88
+ } else {
89
+ this.setState({matching: this.props.all_items});
90
+ }
91
+ }
92
+
93
+ closeList () {
94
+ this.setState({matching: null});
95
+ }
96
+
97
+ onSelected (value) {
98
+ this.setState({text: value, matching: null});
99
+ this.props.onSelect(value);
100
+ }
101
+
102
+ }
103
+
104
+ class DropdownList extends preact.Component {
105
+
106
+ constructor (props) {
107
+ super(props);
108
+ this.state = {selected: -1};
109
+ }
110
+
111
+ render ({app, items, toSelect}, {selected}) {
112
+ return <div
113
+ className="_dropdown">
114
+ <ul>
115
+ {items.map((name, i) => <li
116
+ className={classnames({'selected': selected === i})}
117
+ onClick={() => toSelect(name)}>
118
+ {name}
119
+ </li>)}
120
+ </ul>
121
+ </div>;
122
+ }
123
+
124
+ onKeyDown (e) {
125
+ switch (e.keyCode) {
126
+ case 40: // down
127
+ this.changeSelection(1);
128
+ return true;
129
+ break;
130
+
131
+ case 38: // up
132
+ this.changeSelection(-1);
133
+ return true;
134
+ break;
135
+
136
+ case 13: // enter
137
+ const selected = this.state.selected;
138
+ if (selected !== -1) this.props.toSelect(this.props.items[selected]);
139
+ return true;
140
+ break;
141
+
142
+ case 27: // escape
143
+ this.props.toClose();
144
+ return true;
145
+ break;
146
+
147
+ }
148
+ return false;
149
+ }
150
+
151
+ changeSelection (value) {
152
+ let selected = this.state.selected;
153
+ selected += value;
154
+ if (selected < 0) selected = 0;
155
+ if (selected >= this.props.items.length) selected = this.props.items.length -1;
156
+ this.setState({selected});
157
+ }
158
+ }
159
+
160
+ function filter_items (all, input_text) {
161
+ let items = all.filter(value => value.startsWith(input_text));
162
+ if (items.length === 0) items = null;
163
+ return items;
164
+ }