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 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
@@ -0,0 +1,2 @@
1
+ # v0.1.0
2
+ * Initial commit
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
@@ -0,0 +1,5 @@
1
+ # Grimoire
2
+
3
+ ## Info
4
+
5
+ * Repository: https://github.com/spox/grimoire
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,14 @@
1
+ require 'grimoire'
2
+
3
+ module Grimoire
4
+ # Constraint resolution path
5
+ class Path < Utility
6
+
7
+ attribute :units, Unit, :multiple => true, :default => []
8
+
9
+ def dependencies
10
+ units.map(&:dependencies).flatten.uniq!
11
+ end
12
+
13
+ end
14
+ 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
@@ -0,0 +1,4 @@
1
+ module Grimoire
2
+ # Current library version
3
+ VERSION = Gem::Version.new('0.1.0')
4
+ 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: