calculi 0.0.1
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/.gitignore +18 -0
- data/.pryrc +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +1 -0
- data/calculi.gemspec +25 -0
- data/lib/calculi.rb +40 -0
- data/lib/calculi/attribute.rb +23 -0
- data/lib/calculi/attribute/abstract.rb +122 -0
- data/lib/calculi/attribute/boolean.rb +15 -0
- data/lib/calculi/attribute/duration.rb +16 -0
- data/lib/calculi/attribute/integer.rb +5 -0
- data/lib/calculi/attribute/procable.rb +9 -0
- data/lib/calculi/attribute/string.rb +5 -0
- data/lib/calculi/attributes.rb +66 -0
- data/lib/calculi/base.rb +6 -0
- data/lib/calculi/caller.rb +40 -0
- data/lib/calculi/function.rb +106 -0
- data/lib/calculi/function_set.rb +81 -0
- data/lib/calculi/has_function_set.rb +44 -0
- data/lib/calculi/has_option_set.rb +40 -0
- data/lib/calculi/option_set.rb +87 -0
- data/lib/calculi/recursion.rb +6 -0
- data/lib/calculi/utility.rb +73 -0
- data/lib/calculi/version.rb +3 -0
- metadata +113 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 50862d54a9637f48c9e2e8b6930598ace106926b
|
4
|
+
data.tar.gz: 4e2d75db644a1ec3d7a39e304f262a28ed748a68
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 969cf0229ef0ffd07d80441f43232e2af5c0999673454096892670864d1fd900a0218047fda379e401b7947c66a67def459c2b383e4673a11b2b1e87285e7aac
|
7
|
+
data.tar.gz: c3f8c31e64520e3c14a39407a71fca4a5a7462546e457267eb749a7e96bbb3ec88d00944470d8b25ad16f414effe82ad10d556f689de7d7e43029bb23969e1b3
|
data/.gitignore
ADDED
data/.pryrc
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Alexa Grey
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Calculi
|
2
|
+
|
3
|
+
Undergoing development. API may change drastically.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'calculi'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install calculi
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/calculi.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'calculi/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "calculi"
|
8
|
+
spec.version = Calculi::VERSION
|
9
|
+
spec.authors = ["Alexa Grey"]
|
10
|
+
spec.email = ["devel@mouse.vc"]
|
11
|
+
spec.description = %q{Calculated functions for metaprogramming.}
|
12
|
+
spec.summary = %q{Calculated functions for metaprogramming.}
|
13
|
+
spec.homepage = "https://github.com/scryptmouse/calculi"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency 'activesupport', '>= 3.2.0'
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
24
|
+
spec.add_development_dependency "rake"
|
25
|
+
end
|
data/lib/calculi.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'tsort'
|
2
|
+
|
3
|
+
require 'active_support'
|
4
|
+
require 'active_support/callbacks'
|
5
|
+
require 'active_support/concern'
|
6
|
+
require 'active_support/core_ext/module/delegation'
|
7
|
+
require 'active_support/core_ext/object/blank'
|
8
|
+
require 'active_support/core_ext/object/try'
|
9
|
+
require 'active_support/core_ext/array/extract_options'
|
10
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
11
|
+
require 'active_support/core_ext/date/calculations'
|
12
|
+
require 'active_support/core_ext/time/calculations'
|
13
|
+
require 'active_support/core_ext/string/inflections'
|
14
|
+
require 'active_support/core_ext/numeric/time'
|
15
|
+
|
16
|
+
module Calculi
|
17
|
+
require 'calculi/version'
|
18
|
+
# Your code goes here...
|
19
|
+
end
|
20
|
+
|
21
|
+
require 'calculi/utility'
|
22
|
+
require 'calculi/recursion'
|
23
|
+
|
24
|
+
require 'calculi/attribute'
|
25
|
+
|
26
|
+
%w[abstract boolean duration integer procable string].each do |type|
|
27
|
+
require "calculi/attribute/#{type}"
|
28
|
+
end
|
29
|
+
|
30
|
+
require 'calculi/option_set'
|
31
|
+
require 'calculi/has_option_set'
|
32
|
+
|
33
|
+
require 'calculi/attributes'
|
34
|
+
|
35
|
+
require 'calculi/base'
|
36
|
+
require 'calculi/caller'
|
37
|
+
require 'calculi/function'
|
38
|
+
require 'calculi/function_set'
|
39
|
+
|
40
|
+
require 'calculi/has_function_set'
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Calculi::Attribute
|
2
|
+
class << self
|
3
|
+
def lookup(type)
|
4
|
+
if valid_type?(type)
|
5
|
+
type
|
6
|
+
elsif type.kind_of?(String) || type.kind_of?(Symbol)
|
7
|
+
type_from_string type
|
8
|
+
else
|
9
|
+
raise "Unknown attribute type: #{type}"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def valid_type?(type_klass)
|
14
|
+
type_klass.kind_of?(Module) && type < Calculi::Attribute::Abstract
|
15
|
+
end
|
16
|
+
|
17
|
+
def type_from_string(type_name)
|
18
|
+
type_name = type_name.to_s.classify.demodulize
|
19
|
+
|
20
|
+
"Calculi::Attribute::#{type_name}".constantize
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# Typed attributes with defaults.
|
2
|
+
# @abstract
|
3
|
+
class Calculi::Attribute::Abstract < Module
|
4
|
+
include Calculi::Utility
|
5
|
+
|
6
|
+
# @!attribute [r] default
|
7
|
+
# Default value for the attribute
|
8
|
+
# @return [Boolean]
|
9
|
+
attr_reader :default
|
10
|
+
|
11
|
+
# @!attribute [r] ivar_name
|
12
|
+
# Name of the instance variable used to store the attribute's value.
|
13
|
+
# @return [String]
|
14
|
+
attr_reader :ivar_name
|
15
|
+
|
16
|
+
# @!attribute [r] name
|
17
|
+
# Name of the attribute
|
18
|
+
# @return [String]
|
19
|
+
attr_reader :name
|
20
|
+
|
21
|
+
# @!attribute [r] setter
|
22
|
+
# Setter to use
|
23
|
+
# @return [Proc]
|
24
|
+
attr_reader :setter
|
25
|
+
|
26
|
+
NO_DEFAULT = Object.new
|
27
|
+
|
28
|
+
NO_DEFAULT.instance_eval do
|
29
|
+
def to_proc
|
30
|
+
-> { nil }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
PASSTHRU = ->(o) { o }
|
35
|
+
|
36
|
+
def initialize(name, options = {}, &setter)
|
37
|
+
@name = name.to_s
|
38
|
+
|
39
|
+
@ivar_name = at_prefixed name
|
40
|
+
|
41
|
+
@default = options.fetch :default, NO_DEFAULT
|
42
|
+
|
43
|
+
@readonly = !!options.fetch(:readonly, false)
|
44
|
+
|
45
|
+
@setter = block_given? ? setter : options.fetch(:setter) { default_setter }
|
46
|
+
|
47
|
+
define_attribute_methods!
|
48
|
+
end
|
49
|
+
|
50
|
+
# @abstract
|
51
|
+
# @return [Proc]
|
52
|
+
def default_setter
|
53
|
+
PASSTHRU
|
54
|
+
end
|
55
|
+
|
56
|
+
def default?
|
57
|
+
default != NO_DEFAULT
|
58
|
+
end
|
59
|
+
|
60
|
+
def inspect
|
61
|
+
"#{self.class}(:name, :default => #{default.inspect})"
|
62
|
+
end
|
63
|
+
|
64
|
+
def readonly?
|
65
|
+
@readonly
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
def default_name
|
70
|
+
@default_name ||= :"default_#{name}"
|
71
|
+
end
|
72
|
+
|
73
|
+
def getter_name
|
74
|
+
@getter_name ||= :"#{name}"
|
75
|
+
end
|
76
|
+
|
77
|
+
def predicate_name
|
78
|
+
@predicate_name ||= :"#{name}?"
|
79
|
+
end
|
80
|
+
|
81
|
+
def setter_name
|
82
|
+
@setter_name ||= :"#{name}="
|
83
|
+
end
|
84
|
+
|
85
|
+
def define_attribute_methods!
|
86
|
+
define_default!
|
87
|
+
define_getter!
|
88
|
+
define_predicate!
|
89
|
+
define_setter! unless readonly?
|
90
|
+
end
|
91
|
+
|
92
|
+
def define_default!
|
93
|
+
method_body = procable?(default) ? default : constantly(default)
|
94
|
+
|
95
|
+
define_method(default_name, &method_body)
|
96
|
+
end
|
97
|
+
|
98
|
+
def define_getter!
|
99
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
100
|
+
def #{getter_name}
|
101
|
+
defined?(#{ivar_name}) ? #{ivar_name} : (#{ivar_name} = #{default_name})
|
102
|
+
end
|
103
|
+
RUBY
|
104
|
+
end
|
105
|
+
|
106
|
+
def define_predicate!
|
107
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
108
|
+
def #{predicate_name}
|
109
|
+
!#{getter_name}.nil?
|
110
|
+
end
|
111
|
+
RUBY
|
112
|
+
end
|
113
|
+
|
114
|
+
def define_setter!
|
115
|
+
ivar = ivar_name
|
116
|
+
cast_value = setter
|
117
|
+
|
118
|
+
define_method setter_name do |new_value|
|
119
|
+
instance_variable_set ivar, cast_value.(new_value)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class Calculi::Attribute::Boolean < Calculi::Attribute::Abstract
|
2
|
+
# @!attribute [r] default
|
3
|
+
# Default value for the attribute
|
4
|
+
# @return [Boolean]
|
5
|
+
|
6
|
+
# @override
|
7
|
+
def default_setter
|
8
|
+
->(o) { !!o }
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
def define_predicate!
|
13
|
+
alias_method predicate_name, getter_name
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class Calculi::Attribute::Duration < Calculi::Attribute::Abstract
|
2
|
+
def default_setter
|
3
|
+
->(new_duration) do
|
4
|
+
case new_duration
|
5
|
+
when Integer then new_duration
|
6
|
+
when Numeric then new_duration.to_i
|
7
|
+
when :hour, :hourly then 1.hour
|
8
|
+
when :day, :daily then 1.day
|
9
|
+
when :week, :weekly then 1.week
|
10
|
+
when Proc then coerce(value.call)
|
11
|
+
else 1.day
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Calculi::Attributes
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
include ActiveSupport::Callbacks
|
5
|
+
include Calculi::Utility
|
6
|
+
include Calculi::HasOptionSet
|
7
|
+
|
8
|
+
included do
|
9
|
+
define_callbacks :initialize, :configure
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(options = {}, &configurator)
|
13
|
+
process_options! options
|
14
|
+
|
15
|
+
run_callbacks :initialize
|
16
|
+
|
17
|
+
configure(&configurator) if block_given?
|
18
|
+
end
|
19
|
+
|
20
|
+
def configure(&configurator)
|
21
|
+
run_callbacks :configure do
|
22
|
+
instance_eval(&configurator)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
module ClassMethods
|
28
|
+
def calculi_attr(name, type, options = {}, &custom_setter)
|
29
|
+
type = Calculi::Attribute.lookup(type)
|
30
|
+
|
31
|
+
include type.new(name, options, &custom_setter)
|
32
|
+
end
|
33
|
+
|
34
|
+
def calculi_boolean(name, options = {})
|
35
|
+
options = { default: true } if options == true
|
36
|
+
|
37
|
+
calculi_attr name, :boolean, options
|
38
|
+
end
|
39
|
+
|
40
|
+
alias_method :calculi_bool, :calculi_boolean
|
41
|
+
|
42
|
+
def calculi_computed(name, &computer_block)
|
43
|
+
define_method(name) do
|
44
|
+
instance_variable_compute(name, &computer_block)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def calculi_duration(name, options = {})
|
49
|
+
calculi_attr name, :duration, options
|
50
|
+
end
|
51
|
+
|
52
|
+
def calculi_integer(name, options = {})
|
53
|
+
calculi_attr name, :integer, options
|
54
|
+
end
|
55
|
+
|
56
|
+
alias_method :calculi_int, :calculi_integer
|
57
|
+
|
58
|
+
def calculi_procable(name, options = {})
|
59
|
+
calculi_attr name, :procable, options
|
60
|
+
end
|
61
|
+
|
62
|
+
def calculi_string(name, options = {})
|
63
|
+
calculi_attr name, :string, options
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/calculi/base.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
class Calculi::Caller
|
2
|
+
# @!attribute [r] originator
|
3
|
+
# @return [Calculi::Function]
|
4
|
+
attr_reader :originator
|
5
|
+
|
6
|
+
# @!attribute [r] context
|
7
|
+
# @return [Object]
|
8
|
+
attr_reader :context
|
9
|
+
|
10
|
+
delegate :key, :function_set, :recursive?, to: :originator
|
11
|
+
delegate :fetch, to: :function_set
|
12
|
+
|
13
|
+
def initialize(originator, context)
|
14
|
+
@originator = originator
|
15
|
+
@context = context
|
16
|
+
end
|
17
|
+
|
18
|
+
def call(other_function_key, *args)
|
19
|
+
prevent_recursion! other_function_key
|
20
|
+
|
21
|
+
fetch(other_function_key).call(context, *args)
|
22
|
+
end
|
23
|
+
|
24
|
+
alias_method :[], :call
|
25
|
+
|
26
|
+
def chain(*other_functions)
|
27
|
+
other_functions.reduce(context) do |last_context, other_function_key|
|
28
|
+
other_fn = fetch(other_function_key)
|
29
|
+
|
30
|
+
other_fn.call last_context
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
def prevent_recursion!(other_function_key)
|
36
|
+
if other_function_key == key && !recursive?
|
37
|
+
raise Calculi::Recursion, other_function_key
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# A calculated function.
|
2
|
+
class Calculi::Function
|
3
|
+
include Calculi::Base
|
4
|
+
|
5
|
+
# @!attribute [r] memoize
|
6
|
+
# @return [Boolean]
|
7
|
+
calculi_bool :memoize
|
8
|
+
|
9
|
+
# @!attribute [r] recursive
|
10
|
+
# @note currently no effect
|
11
|
+
# @todo implement trampoline or some pseudostack to recur?
|
12
|
+
# @return [Boolean]
|
13
|
+
calculi_bool :recursive
|
14
|
+
|
15
|
+
options! :key, :function_set, memoize: false
|
16
|
+
|
17
|
+
# @param context the object that will actually be _calling_ the function, i.e.
|
18
|
+
# what the value of `self` should be.
|
19
|
+
# @param args any arguments to pass to the body of the function
|
20
|
+
def call(context, *args)
|
21
|
+
fn_caller = Calculi::Caller.new(self, context)
|
22
|
+
|
23
|
+
context.instance_exec(target, fn_caller, args, &body)
|
24
|
+
end
|
25
|
+
|
26
|
+
def eql?(other)
|
27
|
+
other.is_a?(Calculi::Function) && other.hash == hash
|
28
|
+
end
|
29
|
+
|
30
|
+
def hash
|
31
|
+
[function_set.hash, key.hash].hash
|
32
|
+
end
|
33
|
+
|
34
|
+
def inspect
|
35
|
+
"Calculi::Function(:#{key}, :dependencies => #{inspect_dependency_names})"
|
36
|
+
end
|
37
|
+
|
38
|
+
def inspect_dependency_names
|
39
|
+
mapped = dependency_names.map do |name|
|
40
|
+
":#{name}"
|
41
|
+
end.join(', ')
|
42
|
+
|
43
|
+
"[#{mapped}]"
|
44
|
+
end
|
45
|
+
|
46
|
+
alias_method :to_s, :inspect
|
47
|
+
|
48
|
+
# @!group DSL methods
|
49
|
+
def body(&body_proc)
|
50
|
+
if block_given?
|
51
|
+
@body = body_proc
|
52
|
+
end
|
53
|
+
|
54
|
+
@body
|
55
|
+
end
|
56
|
+
|
57
|
+
def name(&name_proc)
|
58
|
+
if block_given?
|
59
|
+
@name = name_proc
|
60
|
+
end
|
61
|
+
|
62
|
+
@name
|
63
|
+
end
|
64
|
+
|
65
|
+
def requires(*deps)
|
66
|
+
dependency_names.merge deps.flatten.map(&:to_s)
|
67
|
+
end
|
68
|
+
# @!endgroup
|
69
|
+
|
70
|
+
def realized_name
|
71
|
+
@realized_name ||= target.instance_eval(&@name)
|
72
|
+
end
|
73
|
+
|
74
|
+
def realized_body
|
75
|
+
#@realized_body ||= begin
|
76
|
+
#__fn = self
|
77
|
+
|
78
|
+
#->(*args) { __fn.call(self, *args) }
|
79
|
+
#end
|
80
|
+
instance_variable_compute 'realized_body' do
|
81
|
+
__fn = self
|
82
|
+
|
83
|
+
->(*args) { __fn.call(self, *args) }
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# @!group Attributes
|
88
|
+
# @!attribute [r] dependency_names
|
89
|
+
# @api private
|
90
|
+
# @return [Set<String>]
|
91
|
+
def dependency_names
|
92
|
+
@dependency_names ||= Set.new
|
93
|
+
end
|
94
|
+
|
95
|
+
# @!attribute [r] function_set
|
96
|
+
# @return [Calculi::FunctionSet]
|
97
|
+
attr_reader :function_set
|
98
|
+
|
99
|
+
# @!attribute [r] key
|
100
|
+
attr_reader :key
|
101
|
+
|
102
|
+
# @!attribute [r] target
|
103
|
+
# @return [Class]
|
104
|
+
delegate :target, to: :function_set
|
105
|
+
# @!endgroup
|
106
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# A set that can hold arbitrarily-computed {Calculi::Function}s
|
2
|
+
# with a dependency graph
|
3
|
+
class Calculi::FunctionSet < Module
|
4
|
+
include Calculi::Base
|
5
|
+
include Enumerable
|
6
|
+
include TSort
|
7
|
+
|
8
|
+
DEPENDENCIES_NOT_CALCULATED = Object.new
|
9
|
+
|
10
|
+
# @!attribute [r] target
|
11
|
+
# Where the generated functions should be accessible.
|
12
|
+
# @return [Class]
|
13
|
+
attr_reader :target
|
14
|
+
|
15
|
+
options! :target
|
16
|
+
|
17
|
+
delegate :each, to: :functions
|
18
|
+
|
19
|
+
set_callback :configure, :after, :define_functions!
|
20
|
+
|
21
|
+
def [](function_key)
|
22
|
+
case function_key
|
23
|
+
when Calculi::Function
|
24
|
+
functions.detect { |fn, deps| fn.eql? function_key }
|
25
|
+
when Symbol, String
|
26
|
+
function_key = function_key.to_s
|
27
|
+
|
28
|
+
functions.detect { |fn, deps| fn.key.to_s == function_key }
|
29
|
+
else
|
30
|
+
raise KeyError, "Don't know how to find function with `#{function_key.inspect}'"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def fetch(fn)
|
35
|
+
self[fn].try(:first) or raise KeyError, "Unknown function: #{fn}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def functions
|
39
|
+
@functions ||= {}
|
40
|
+
end
|
41
|
+
|
42
|
+
def function_names
|
43
|
+
@function_names ||= OpenStruct.new
|
44
|
+
end
|
45
|
+
|
46
|
+
def function(key, options = {}, &function_configurator)
|
47
|
+
options.merge! function_set: self, key: key
|
48
|
+
|
49
|
+
new_function = Calculi::Function.new(options, &function_configurator)
|
50
|
+
|
51
|
+
functions[new_function] = DEPENDENCIES_NOT_CALCULATED
|
52
|
+
end
|
53
|
+
|
54
|
+
def inspect
|
55
|
+
function_hash = functions.each_key.each_with_object({}) do |fn, fn_hsh|
|
56
|
+
fn_hsh[fn.key] = fn.inspect_dependency_names
|
57
|
+
end
|
58
|
+
|
59
|
+
"Calculi::FunctionSet(#{function_hash.inspect})"
|
60
|
+
end
|
61
|
+
|
62
|
+
def tsort_each_node(&block)
|
63
|
+
functions.each_key(&block)
|
64
|
+
end
|
65
|
+
|
66
|
+
def tsort_each_child(node, &block)
|
67
|
+
fetch(node).dependency_names.each do |dependency_name|
|
68
|
+
yield fetch(dependency_name)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def define_functions!
|
73
|
+
tsort.each do |fn|
|
74
|
+
function_names[fn.key] = fn.realized_name
|
75
|
+
|
76
|
+
redefine_method(fn.realized_name, &fn.realized_body)
|
77
|
+
end
|
78
|
+
|
79
|
+
target.send :include, self
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Calculi::HasFunctionSet
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
include Calculi::Utility
|
5
|
+
include Calculi::HasOptionSet
|
6
|
+
include Calculi::Attributes
|
7
|
+
|
8
|
+
included do
|
9
|
+
set_callback :initialize, :after, :realize_calculi_function_set!
|
10
|
+
|
11
|
+
delegate :function_names, to: :function_set
|
12
|
+
end
|
13
|
+
|
14
|
+
def realize_calculi_function_set!
|
15
|
+
calculi_function_set.configure(&stored_calculi_function_set_configurator)
|
16
|
+
end
|
17
|
+
|
18
|
+
# @!attribute [r] calculi_function_set
|
19
|
+
# @api private
|
20
|
+
# @return [Calculi::FunctionSet]
|
21
|
+
def calculi_function_set
|
22
|
+
@calculi_function_set ||= Calculi::FunctionSet.new target: self
|
23
|
+
end
|
24
|
+
|
25
|
+
def stored_calculi_function_set_configurator
|
26
|
+
self.class.stored_calculi_function_set_configurator
|
27
|
+
end
|
28
|
+
|
29
|
+
module ClassMethods
|
30
|
+
# @!attribute [rw] stored_calculi_function_set_configurator
|
31
|
+
attr_accessor :stored_calculi_function_set_configurator
|
32
|
+
|
33
|
+
# @api private
|
34
|
+
def calculi_functions(&configurator)
|
35
|
+
@stored_calculi_function_set_configurator = configurator if block_given?
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
# @api private
|
40
|
+
def calculi_function_set
|
41
|
+
@calculi_function_set ||= Calculi::FunctionSet.new target: self
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Calculi::HasOptionSet
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
#delegate :option_set, to: :class
|
6
|
+
end
|
7
|
+
|
8
|
+
def option_set
|
9
|
+
self.class.option_set
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
# @param [Hash] options
|
14
|
+
# @return [void]
|
15
|
+
def process_options!(options = {})
|
16
|
+
option_set.process!(self, options)
|
17
|
+
end
|
18
|
+
|
19
|
+
module ClassMethods
|
20
|
+
def option_set
|
21
|
+
@option_set ||= Calculi::OptionSet.new
|
22
|
+
end
|
23
|
+
|
24
|
+
def option!(name, default_value = Calculi::OptionSet::REQUIRED, &default_proc)
|
25
|
+
option_set[name] = block_given? ? default_proc : default_value
|
26
|
+
end
|
27
|
+
|
28
|
+
def options!(*names)
|
29
|
+
names = names.flatten
|
30
|
+
|
31
|
+
names_with_defaults = names.extract_options!
|
32
|
+
|
33
|
+
names_with_defaults.each do |name, default|
|
34
|
+
option_set[name] = default
|
35
|
+
end
|
36
|
+
|
37
|
+
option_set.add(*names)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
class Calculi::OptionSet
|
2
|
+
include Calculi::Utility
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
REQUIRED = Object.new
|
6
|
+
|
7
|
+
# @!attribute [r] defined_options
|
8
|
+
# @api private
|
9
|
+
# @return [Hash]
|
10
|
+
attr_reader :defined_options
|
11
|
+
|
12
|
+
delegate :each, to: :sorted_options
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@defined_options = {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def add(*option_names)
|
19
|
+
option_names.flatten.each do |name|
|
20
|
+
define name
|
21
|
+
end
|
22
|
+
|
23
|
+
return self
|
24
|
+
end
|
25
|
+
|
26
|
+
def <<(option_name)
|
27
|
+
define option_name
|
28
|
+
|
29
|
+
return self
|
30
|
+
end
|
31
|
+
|
32
|
+
def define(option_name, default_value = REQUIRED)
|
33
|
+
defined_options.store(option_name, default_value)
|
34
|
+
|
35
|
+
clear_sorted!
|
36
|
+
|
37
|
+
return self
|
38
|
+
end
|
39
|
+
|
40
|
+
alias_method :[]=, :define
|
41
|
+
|
42
|
+
def fetch(option_name)
|
43
|
+
defined_options.fetch option_name
|
44
|
+
end
|
45
|
+
|
46
|
+
alias_method :[], :fetch
|
47
|
+
|
48
|
+
def process!(context, options)
|
49
|
+
options = options.with_indifferent_access
|
50
|
+
|
51
|
+
each do |name, default_value|
|
52
|
+
value = options.fetch name do
|
53
|
+
eval_or_value default_value, context: context
|
54
|
+
end
|
55
|
+
|
56
|
+
raise ArgumentError, "Missing required attribute: #{name}" if required?(value)
|
57
|
+
|
58
|
+
set_attribute(name, value, context: context)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
def clear_sorted!
|
64
|
+
@sorted_options = nil
|
65
|
+
end
|
66
|
+
|
67
|
+
def required?(value)
|
68
|
+
REQUIRED === value
|
69
|
+
end
|
70
|
+
|
71
|
+
def sorted_options
|
72
|
+
@sorted_options ||= Hash[sort_options]
|
73
|
+
end
|
74
|
+
|
75
|
+
def sort_options
|
76
|
+
defined_options.sort do |(k1, v1), (k2, v2)|
|
77
|
+
r1 = required? v1
|
78
|
+
r2 = required? v2
|
79
|
+
|
80
|
+
if r1 ^ r2
|
81
|
+
r1 ? -1 : 1
|
82
|
+
else
|
83
|
+
k1 <=> k2
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Calculi::Utility
|
2
|
+
NON_IVAR = /\A([^@].+)\z/
|
3
|
+
NULL = Object.new
|
4
|
+
|
5
|
+
# @param [#to_s] s what to prefix
|
6
|
+
# @return [String] `@s`
|
7
|
+
def at_prefixed(s)
|
8
|
+
s.to_s.sub NON_IVAR, '@\1'
|
9
|
+
end
|
10
|
+
|
11
|
+
# @param [Object] o
|
12
|
+
# @return [Proc] f(o) = -> o
|
13
|
+
def constantly(o)
|
14
|
+
-> { o }
|
15
|
+
end
|
16
|
+
|
17
|
+
# @param [#call] thing
|
18
|
+
# @return [Boolean] whether the provided `thing` is callable
|
19
|
+
def callable?(thing)
|
20
|
+
thing.respond_to? :call
|
21
|
+
end
|
22
|
+
|
23
|
+
# @param [#to_proc] callable
|
24
|
+
# @return
|
25
|
+
def eval_or_value(callable, *args)
|
26
|
+
options = args.extract_options!
|
27
|
+
|
28
|
+
context = options.fetch :context, self
|
29
|
+
|
30
|
+
if callable.respond_to?(:to_proc)
|
31
|
+
args.length.nonzero? ? context.instance_exec(*args, &callable) : context.instance_eval(&callable)
|
32
|
+
else
|
33
|
+
callable
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def instance_variable_compute(ivar_name, computed_value = NULL, &compute_value)
|
38
|
+
ivar_name = at_prefixed ivar_name
|
39
|
+
|
40
|
+
computed_value_given = computed_value != NULL
|
41
|
+
value_computer_given = block_given?
|
42
|
+
|
43
|
+
if instance_variable_defined? ivar_name
|
44
|
+
return instance_variable_get ivar_name
|
45
|
+
end
|
46
|
+
|
47
|
+
if computed_value_given ^ value_computer_given
|
48
|
+
instance_variable_set ivar_name, eval_or_value(compute_value)
|
49
|
+
elsif computed_value_given && value_computer_given
|
50
|
+
raise ArgumentError, "Cannot have both computer and computed value"
|
51
|
+
else
|
52
|
+
raise ArgumentError, "Missing compute_value block or computed_value"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# @param [#to_proc] thing
|
57
|
+
# @return [Boolean] whether the provided `thing` can be converted to a proc
|
58
|
+
def procable?(thing)
|
59
|
+
thing.respond_to? :to_proc
|
60
|
+
end
|
61
|
+
|
62
|
+
def set_attribute(name, value, options = {})
|
63
|
+
context = options.fetch :context, self
|
64
|
+
|
65
|
+
setter = "#{name}="
|
66
|
+
|
67
|
+
if context.respond_to? setter
|
68
|
+
context.__send__(setter, value)
|
69
|
+
else
|
70
|
+
context.instance_variable_set at_prefixed(name), value
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
metadata
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: calculi
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Alexa Grey
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-04-23 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 3.2.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 3.2.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.3'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.3'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
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: Calculated functions for metaprogramming.
|
56
|
+
email:
|
57
|
+
- devel@mouse.vc
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- .gitignore
|
63
|
+
- .pryrc
|
64
|
+
- Gemfile
|
65
|
+
- LICENSE.txt
|
66
|
+
- README.md
|
67
|
+
- Rakefile
|
68
|
+
- calculi.gemspec
|
69
|
+
- lib/calculi.rb
|
70
|
+
- lib/calculi/attribute.rb
|
71
|
+
- lib/calculi/attribute/abstract.rb
|
72
|
+
- lib/calculi/attribute/boolean.rb
|
73
|
+
- lib/calculi/attribute/duration.rb
|
74
|
+
- lib/calculi/attribute/integer.rb
|
75
|
+
- lib/calculi/attribute/procable.rb
|
76
|
+
- lib/calculi/attribute/string.rb
|
77
|
+
- lib/calculi/attributes.rb
|
78
|
+
- lib/calculi/base.rb
|
79
|
+
- lib/calculi/caller.rb
|
80
|
+
- lib/calculi/function.rb
|
81
|
+
- lib/calculi/function_set.rb
|
82
|
+
- lib/calculi/has_function_set.rb
|
83
|
+
- lib/calculi/has_option_set.rb
|
84
|
+
- lib/calculi/option_set.rb
|
85
|
+
- lib/calculi/recursion.rb
|
86
|
+
- lib/calculi/utility.rb
|
87
|
+
- lib/calculi/version.rb
|
88
|
+
homepage: https://github.com/scryptmouse/calculi
|
89
|
+
licenses:
|
90
|
+
- MIT
|
91
|
+
metadata: {}
|
92
|
+
post_install_message:
|
93
|
+
rdoc_options: []
|
94
|
+
require_paths:
|
95
|
+
- lib
|
96
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
97
|
+
requirements:
|
98
|
+
- - '>='
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: '0'
|
101
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - '>='
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: '0'
|
106
|
+
requirements: []
|
107
|
+
rubyforge_project:
|
108
|
+
rubygems_version: 2.1.4
|
109
|
+
signing_key:
|
110
|
+
specification_version: 4
|
111
|
+
summary: Calculated functions for metaprogramming.
|
112
|
+
test_files: []
|
113
|
+
has_rdoc:
|