admission 0.1.9 → 0.2.0

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.
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
+ }