grimoire 0.1.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 +7 -0
- data/CHANGELOG.md +2 -0
- data/CONTRIBUTING.md +25 -0
- data/LICENSE +13 -0
- data/README.md +5 -0
- data/grimoire.gemspec +17 -0
- data/lib/grimoire.rb +24 -0
- data/lib/grimoire/error.rb +19 -0
- data/lib/grimoire/path.rb +14 -0
- data/lib/grimoire/requirement_list.rb +11 -0
- data/lib/grimoire/solver.rb +186 -0
- data/lib/grimoire/system.rb +67 -0
- data/lib/grimoire/unit.rb +14 -0
- data/lib/grimoire/utility.rb +21 -0
- data/lib/grimoire/version.rb +4 -0
- metadata +100 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ef5df1a91ba2a0ba8d102c0566e2eccca125678c
|
4
|
+
data.tar.gz: 7f2faf3d48426e22fee9ddf77d2c7845d1897071
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 67788092dff047186519fff5efe33869c3395bda3769f49de24dca62d8a5e2a466d11278fbd770ed0cb14b212f782321a5c90c37ff8c9e6f555bfb68201a50cf
|
7
|
+
data.tar.gz: 548afdfb3c061bb89591113404c438e97d9ecf4ed7cb74b4cf5b843c4c2a820e998218bb3e3f2b72fbc5a262cf99c0ed61168f9f354cb7abd6c3ab76fb3f6125
|
data/CHANGELOG.md
ADDED
data/CONTRIBUTING.md
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# Contributing
|
2
|
+
|
3
|
+
## Branches
|
4
|
+
|
5
|
+
### `master` branch
|
6
|
+
|
7
|
+
The master branch is the current stable released version.
|
8
|
+
|
9
|
+
### `develop` branch
|
10
|
+
|
11
|
+
The develop branch is the current edge of development.
|
12
|
+
|
13
|
+
## Pull requests
|
14
|
+
|
15
|
+
* https://github.com/spox/grimoire
|
16
|
+
|
17
|
+
Please base all pull requests of the `develop` branch. Merges to
|
18
|
+
`master` only occur through the `develop` branch. Pull requests
|
19
|
+
based on `master` will likely be cherry picked.
|
20
|
+
|
21
|
+
## Issues
|
22
|
+
|
23
|
+
Need to report an issue? Use the github issues:
|
24
|
+
|
25
|
+
* https://github.com/spox/grimoire
|
data/LICENSE
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright 2015 Chris Roberts
|
2
|
+
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
you may not use this file except in compliance with the License.
|
5
|
+
You may obtain a copy of the License at
|
6
|
+
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
See the License for the specific language governing permissions and
|
13
|
+
limitations under the License.
|
data/README.md
ADDED
data/grimoire.gemspec
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__)) + '/lib/'
|
2
|
+
require 'grimoire/version'
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = 'grimoire'
|
5
|
+
s.version = Grimoire::VERSION.version
|
6
|
+
s.summary = 'Constraint solver'
|
7
|
+
s.author = 'Chris Roberts'
|
8
|
+
s.email = 'code@chrisroberts.org'
|
9
|
+
s.homepage = 'https://github.com/spox/grimoire'
|
10
|
+
s.description = 'Specialized constraint solver allowing weighted results'
|
11
|
+
s.require_path = 'lib'
|
12
|
+
s.license = 'Apache 2.0'
|
13
|
+
s.add_runtime_dependency 'bogo', '~> 0.1.10'
|
14
|
+
s.add_development_dependency 'minitest'
|
15
|
+
s.add_development_dependency 'pry'
|
16
|
+
s.files = Dir['{lib}/**/**/*'] + %w(grimoire.gemspec README.md CHANGELOG.md CONTRIBUTING.md LICENSE)
|
17
|
+
end
|
data/lib/grimoire.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'bogo'
|
2
|
+
|
3
|
+
module Grimoire
|
4
|
+
# @todo Provide abstract interfaces for these and run validation on
|
5
|
+
# defined classes to ensure expected methods
|
6
|
+
|
7
|
+
# Class used to define version information
|
8
|
+
VERSION_CLASS = Gem::Version
|
9
|
+
# Class used to define dependency information
|
10
|
+
DEPENDENCY_CLASS = Gem::Dependency
|
11
|
+
# Class used to define requirement
|
12
|
+
REQUIREMENT_CLASS = Gem::Requirement
|
13
|
+
|
14
|
+
autoload :Error, 'grimoire/error'
|
15
|
+
autoload :Path, 'grimoire/path'
|
16
|
+
autoload :RequirementList, 'grimoire/requirement_list'
|
17
|
+
autoload :Solver, 'grimoire/solver'
|
18
|
+
autoload :System, 'grimoire/system'
|
19
|
+
autoload :Unit, 'grimoire/unit'
|
20
|
+
autoload :Utility, 'grimoire/utility'
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
require 'grimoire/version'
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'grimoire'
|
2
|
+
|
3
|
+
module Grimoire
|
4
|
+
|
5
|
+
# General error
|
6
|
+
class Error < StandardError
|
7
|
+
# Requested unit is unavailable
|
8
|
+
class UnitUnavailable < Error
|
9
|
+
attr_accessor :unit_name
|
10
|
+
end
|
11
|
+
# Resolution path is not valid within constraints
|
12
|
+
class ResolutionPathInvalid < Error; end
|
13
|
+
# Too many loops run during path generation
|
14
|
+
class MaximumGenerationLoopsExceeded < Error; end
|
15
|
+
# No valid solution available
|
16
|
+
class NoSolution < Error; end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'grimoire'
|
2
|
+
|
3
|
+
module Grimoire
|
4
|
+
|
5
|
+
# Requirment list for solver
|
6
|
+
class RequirementList < Utility
|
7
|
+
attribute :name, String, :required => true, :coerce => lambda{|val| val.to_s}
|
8
|
+
attribute :requirements, DEPENDENCY_CLASS, :multiple => true, :default => [], :coerce => lambda{|val| DEPENDENCY_CLASS.new(val.first, *val.last)}
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
require 'grimoire'
|
2
|
+
|
3
|
+
module Grimoire
|
4
|
+
# Requirement solver
|
5
|
+
class Solver < Utility
|
6
|
+
|
7
|
+
# Ceiling for number of loops allowed during path generation
|
8
|
+
MAX_GENERATION_LOOPS = 100000
|
9
|
+
|
10
|
+
include Bogo::Memoization
|
11
|
+
|
12
|
+
attribute :requirements, RequirementList, :required => true
|
13
|
+
attribute :system, System, :required => true
|
14
|
+
|
15
|
+
# @return [System] subset of full system based on requirements
|
16
|
+
attr_reader :world
|
17
|
+
|
18
|
+
def initialize(*_)
|
19
|
+
super
|
20
|
+
@world = System.new
|
21
|
+
build_world(requirements.requirements, world, system)
|
22
|
+
world.scrub!
|
23
|
+
end
|
24
|
+
|
25
|
+
# Build the world required for the solver (the subset of the
|
26
|
+
# entire system required by the requirements)
|
27
|
+
#
|
28
|
+
# @param deps [DEPENDENCY_CLASS] dependencies required to resolve
|
29
|
+
# @param my_world [System] system to populate with units
|
30
|
+
# @param root [System] superset system to extract units
|
31
|
+
def build_world(deps=nil, my_world=nil, root = nil)
|
32
|
+
deps = requirement.requirements unless deps
|
33
|
+
my_world = world unless my_world
|
34
|
+
root = system unless root
|
35
|
+
deps.each do |dep|
|
36
|
+
units = root.subset(dep.name, dep.requirement)
|
37
|
+
units.each do |unit|
|
38
|
+
build_world(unit.dependencies, my_world, root)
|
39
|
+
end
|
40
|
+
my_world.add_unit(units)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# @return [Smash<{Unit#name:Bogo::PriorityQueue}>]
|
45
|
+
def queues
|
46
|
+
memoize(:queues) do
|
47
|
+
Smash[
|
48
|
+
world.units.map do |name, items|
|
49
|
+
queue = Bogo::PriorityQueue.new
|
50
|
+
populate_queue(queue, items)
|
51
|
+
[name, queue]
|
52
|
+
end
|
53
|
+
]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Populate queue with units
|
58
|
+
#
|
59
|
+
# @param p_queue [Bogo::PriorityQueue]
|
60
|
+
# @param units [Array<Unit>]
|
61
|
+
# @return [Bogo::PriorityQueue]
|
62
|
+
def populate_queue(p_queue, units)
|
63
|
+
units.each_with_index do |unit, index|
|
64
|
+
p_queue.push(unit, score_unit(unit, index))
|
65
|
+
end
|
66
|
+
p_queue
|
67
|
+
end
|
68
|
+
|
69
|
+
# Provide score for given unit
|
70
|
+
#
|
71
|
+
# @param unit [Unit]
|
72
|
+
# @param score [Integer] current score
|
73
|
+
# @return [Integer] score
|
74
|
+
def score_unit(unit, score)
|
75
|
+
score
|
76
|
+
end
|
77
|
+
|
78
|
+
# Repopulate the given queue
|
79
|
+
#
|
80
|
+
# @param name [String]
|
81
|
+
# @return [self]
|
82
|
+
def reset_queue(name)
|
83
|
+
queue = populate_queue(
|
84
|
+
Bogo::PriorityQueue.new,
|
85
|
+
world.units[name]
|
86
|
+
)
|
87
|
+
queues[name] = queue
|
88
|
+
self
|
89
|
+
end
|
90
|
+
|
91
|
+
# Provide Unit acceptable for given dependency
|
92
|
+
#
|
93
|
+
# @param dep [DEPENDENCY_CLASS]
|
94
|
+
# @return [Unit]
|
95
|
+
# @raises [Error::UnitUnavailable]
|
96
|
+
def unit_for(dep)
|
97
|
+
unit = nil
|
98
|
+
until(unit || queues[dep.name].empty?)
|
99
|
+
unit = queues[dep.name].pop
|
100
|
+
unit = nil unless dep.requirement.satisfied_by?(unit.version)
|
101
|
+
end
|
102
|
+
unless(unit)
|
103
|
+
error = Error::UnitUnavailable.new("Failed to locate valid unit for: #{dep.inspect}")
|
104
|
+
error.unit_name = dep.name
|
105
|
+
raise error
|
106
|
+
end
|
107
|
+
unit
|
108
|
+
end
|
109
|
+
|
110
|
+
# Resolve path for a given dependency
|
111
|
+
#
|
112
|
+
# @param dep [DEPENDENCY_CLASS]
|
113
|
+
# @param given [DEPENDENCY_CLASS]
|
114
|
+
# @return [Array<Unit>]
|
115
|
+
def resolve(dep, given=nil)
|
116
|
+
unit = given || unit_for(dep)
|
117
|
+
if(unit.dependencies.empty?)
|
118
|
+
[unit]
|
119
|
+
else
|
120
|
+
deps = [unit]
|
121
|
+
begin
|
122
|
+
unit.dependencies.map do |u_dep|
|
123
|
+
existing = deps.detect{|d| d.name == u_dep.name}
|
124
|
+
if(existing)
|
125
|
+
if(u_dep.requirement.satisfied_by?(existing.version))
|
126
|
+
next
|
127
|
+
else
|
128
|
+
deps.delete(existing)
|
129
|
+
reset_queue(u_dep.name) unless given
|
130
|
+
end
|
131
|
+
else
|
132
|
+
reset_queue(u_dep.name) unless given
|
133
|
+
end
|
134
|
+
deps += resolve(u_dep)
|
135
|
+
deps.compact!
|
136
|
+
u_dep
|
137
|
+
end.compact.map do |u_dep| # validator
|
138
|
+
existing = deps.detect{|d| d.name == u_dep.name}
|
139
|
+
if(existing)
|
140
|
+
unless(u_dep.requirement.satisfied_by?(existing.version))
|
141
|
+
deps.delete(existing)
|
142
|
+
reset_queue(u_dep.name)
|
143
|
+
raise Error::ResolutionPathInvalid.new("Unit <#{existing.inspect}> does not satisfy <#{u_dep.inspect}>")
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
rescue Error::ResolutionPathInvalid
|
148
|
+
retry
|
149
|
+
end
|
150
|
+
deps
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Generate valid constraint paths
|
155
|
+
#
|
156
|
+
# @return [Bogo::PriorityQueue<Path>]
|
157
|
+
def generate!
|
158
|
+
custom_unit = Unit.new(
|
159
|
+
:name => '~_SOLVER_UNIT_~',
|
160
|
+
:version => '1.0.0',
|
161
|
+
:dependencies => requirements.requirements
|
162
|
+
)
|
163
|
+
count = 0
|
164
|
+
results = Bogo::PriorityQueue.new
|
165
|
+
begin
|
166
|
+
until(count > MAX_GENERATION_LOOPS)
|
167
|
+
result = resolve(nil, custom_unit)
|
168
|
+
results.push(Path.new(:units => result.slice(1, result.size)), count)
|
169
|
+
count += 1
|
170
|
+
end
|
171
|
+
rescue Error::UnitUnavailable
|
172
|
+
count = nil
|
173
|
+
end
|
174
|
+
unless(count.nil?)
|
175
|
+
raise Error::MaximumGenerationLoopsExceeded.new("Exceeded maximum allowed loops for path generation: #{MAX_GENERATION_LOOPS}")
|
176
|
+
else
|
177
|
+
if(results.empty?)
|
178
|
+
raise Error::NoSolution.new("Failed to generate valid path for requirements: `#{custom_unit.dependencies.inspect}`")
|
179
|
+
else
|
180
|
+
results
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
end
|
186
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'grimoire'
|
2
|
+
|
3
|
+
module Grimoire
|
4
|
+
# Contains all available Units
|
5
|
+
class System
|
6
|
+
|
7
|
+
# @return [Smash]
|
8
|
+
attr_reader :units
|
9
|
+
|
10
|
+
# Create new system
|
11
|
+
#
|
12
|
+
# @return [self]
|
13
|
+
def initialize(*_)
|
14
|
+
@units = Smash.new
|
15
|
+
end
|
16
|
+
|
17
|
+
# Register new unit
|
18
|
+
#
|
19
|
+
# @param unit [Unit]
|
20
|
+
# @return [self]
|
21
|
+
def add_unit(*unit)
|
22
|
+
[unit].flatten.compact.each do |u|
|
23
|
+
unless(units[u.name])
|
24
|
+
units[u.name] = []
|
25
|
+
end
|
26
|
+
units[u.name].push(u) unless units[u.name].include?(u)
|
27
|
+
end
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
# Remove registered unit
|
32
|
+
#
|
33
|
+
# @param unit [Unit]
|
34
|
+
# @param deps
|
35
|
+
# @return [self]
|
36
|
+
def remove_unit(unit)
|
37
|
+
if(units[unit.name])
|
38
|
+
units[unit.name].delete(unit)
|
39
|
+
end
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
# Provide all available units that satisfy the constraint
|
44
|
+
#
|
45
|
+
# @param unit_name [String]
|
46
|
+
# @param constraint [REQUIREMENT_CLASS]
|
47
|
+
# @return [Array<Unit>]
|
48
|
+
def subset(unit_name, constraint)
|
49
|
+
units[unit_name].find_all do |unit|
|
50
|
+
constraint.satisfied_by?(unit.version)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Removes any duplicate units registered
|
55
|
+
# and sorts all unit lists
|
56
|
+
#
|
57
|
+
# @return [self]
|
58
|
+
def scrub!
|
59
|
+
units.values.map do |items|
|
60
|
+
items.sort!{|x,y| y.version <=> x.version}
|
61
|
+
items.uniq!
|
62
|
+
end
|
63
|
+
self
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'grimoire'
|
2
|
+
|
3
|
+
module Grimoire
|
4
|
+
|
5
|
+
# Defines a specific unit in the system
|
6
|
+
class Unit < Utility
|
7
|
+
|
8
|
+
attribute :name, String, :required => true
|
9
|
+
attribute :dependencies, DEPENDENCY_CLASS, :multiple => true, :default => [], :coerce => lambda{|val| DEPENDENCY_CLASS.new(val.first, *val.last)}
|
10
|
+
attribute :version, VERSION_CLASS, :required => true, :coerce => lambda{|val| VERSION_CLASS.new(val)}
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'grimoire'
|
2
|
+
|
3
|
+
module Grimoire
|
4
|
+
|
5
|
+
# Base class for building utility objects
|
6
|
+
class Utility
|
7
|
+
|
8
|
+
# Provide lazy setup helpers
|
9
|
+
include Bogo::Lazy
|
10
|
+
|
11
|
+
# Disable data state
|
12
|
+
always_clean!
|
13
|
+
|
14
|
+
# Force load on init to enforce rules
|
15
|
+
def initialize(args={})
|
16
|
+
load_data(args)
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
metadata
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: grimoire
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Chris Roberts
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-02-25 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bogo
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.1.10
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.1.10
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: minitest
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: pry
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description: Specialized constraint solver allowing weighted results
|
56
|
+
email: code@chrisroberts.org
|
57
|
+
executables: []
|
58
|
+
extensions: []
|
59
|
+
extra_rdoc_files: []
|
60
|
+
files:
|
61
|
+
- CHANGELOG.md
|
62
|
+
- CONTRIBUTING.md
|
63
|
+
- LICENSE
|
64
|
+
- README.md
|
65
|
+
- grimoire.gemspec
|
66
|
+
- lib/grimoire.rb
|
67
|
+
- lib/grimoire/error.rb
|
68
|
+
- lib/grimoire/path.rb
|
69
|
+
- lib/grimoire/requirement_list.rb
|
70
|
+
- lib/grimoire/solver.rb
|
71
|
+
- lib/grimoire/system.rb
|
72
|
+
- lib/grimoire/unit.rb
|
73
|
+
- lib/grimoire/utility.rb
|
74
|
+
- lib/grimoire/version.rb
|
75
|
+
homepage: https://github.com/spox/grimoire
|
76
|
+
licenses:
|
77
|
+
- Apache 2.0
|
78
|
+
metadata: {}
|
79
|
+
post_install_message:
|
80
|
+
rdoc_options: []
|
81
|
+
require_paths:
|
82
|
+
- lib
|
83
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
89
|
+
requirements:
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
requirements: []
|
94
|
+
rubyforge_project:
|
95
|
+
rubygems_version: 2.2.2
|
96
|
+
signing_key:
|
97
|
+
specification_version: 4
|
98
|
+
summary: Constraint solver
|
99
|
+
test_files: []
|
100
|
+
has_rdoc:
|