frappuccino 0.0.1 → 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 +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +8 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +104 -0
- data/Rakefile +12 -0
- data/frappuccino.gemspec +23 -0
- data/lib/frappuccino.rb +3 -140
- data/lib/frappuccino/property.rb +16 -0
- data/lib/frappuccino/source.rb +14 -0
- data/lib/frappuccino/stream.rb +112 -0
- data/lib/frappuccino/stream/drop.rb +18 -0
- data/lib/frappuccino/stream/map.rb +12 -0
- data/lib/frappuccino/stream/scan.rb +15 -0
- data/lib/frappuccino/stream/select.rb +14 -0
- data/lib/frappuccino/stream/take.rb +16 -0
- data/lib/frappuccino/stream/zip.rb +26 -0
- data/lib/frappuccino/version.rb +3 -0
- data/test/drop_test.rb +13 -0
- data/test/map_test.rb +80 -0
- data/test/merge_test.rb +53 -0
- data/test/mvp_test.rb +22 -0
- data/test/not_implemented_test.rb +18 -0
- data/test/on_value_test.rb +75 -0
- data/test/property_test.rb +22 -0
- data/test/scan_test.rb +14 -0
- data/test/select_test.rb +40 -0
- data/test/source_test.rb +14 -0
- data/test/stream_test.rb +44 -0
- data/test/take_test.rb +13 -0
- data/test/test_helper.rb +66 -0
- data/test/zip_test.rb +64 -0
- metadata +106 -46
- data/bin/frappuccino +0 -8
- data/template/bg.png +0 -0
- data/template/index.erb +0 -191
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 3aa6c347af81d9a074803f7be38c360db41adee4
|
4
|
+
data.tar.gz: 49da3f4765330f9ad73de449aa3080454f9f49e2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 68c592a44ab4e948adfa4b8bb4214438cf3282cd9a3ce6047c436ad19471fa440b7d9703afc3fd0d9da386cc2873cc9091b7d80abdc8436973df00d0a79b4c40
|
7
|
+
data.tar.gz: cf1dc7f6d0394e3587714d668ab2a9aa304089ab4c5c239b7bbb8f406818ead40e6aa35dcc6c81c07ee236294090911018531eb3f96aa4484b5a0d902f420a04
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Steve Klabnik
|
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,104 @@
|
|
1
|
+
# Frappuccino
|
2
|
+
|
3
|
+
Functional Reactive Programming for Ruby.
|
4
|
+
|
5
|
+
[](https://travis-ci.org/steveklabnik/frappuccino) [](https://codeclimate.com/github/steveklabnik/frappuccino) [](https://coveralls.io/r/steveklabnik/frappuccino)
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'frappuccino', github: "steveklabnik/frappuccino"
|
13
|
+
```
|
14
|
+
|
15
|
+
(I'm hoping that @yoka will give me the gem name, until then, you
|
16
|
+
must install from GitHub.)
|
17
|
+
|
18
|
+
And then execute:
|
19
|
+
|
20
|
+
$ bundle
|
21
|
+
|
22
|
+
Or install it yourself as:
|
23
|
+
|
24
|
+
$ git clone https://github.com/steveklabnik/frappuccino
|
25
|
+
$ cd frappuccino
|
26
|
+
$ bundle
|
27
|
+
$ rake install
|
28
|
+
|
29
|
+
## Usage
|
30
|
+
|
31
|
+
Basically, this:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
require 'frappuccino'
|
35
|
+
|
36
|
+
class Button
|
37
|
+
def push
|
38
|
+
emit(:pushed) # emit sends a value into the stream
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
button = Button.new
|
43
|
+
stream = Frappuccino::Stream.new(button)
|
44
|
+
|
45
|
+
counter = stream
|
46
|
+
.map {|event| event == :pushed ? 1 : 0 } # convert events to ints
|
47
|
+
.inject(0) {|sum, n| sum + n } # add them up
|
48
|
+
|
49
|
+
counter.now # => 0
|
50
|
+
|
51
|
+
button.push
|
52
|
+
button.push
|
53
|
+
button.push
|
54
|
+
|
55
|
+
counter.now # => 3
|
56
|
+
|
57
|
+
button.push
|
58
|
+
|
59
|
+
counter.now # => 4
|
60
|
+
```
|
61
|
+
|
62
|
+
You can also map via a hash, if you prefer:
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
.map(:pushed => 1, :default => 0)
|
66
|
+
```
|
67
|
+
|
68
|
+
You can also register callbacks to a Stream. These will executed for
|
69
|
+
each event that occurs in the Stream:
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
stream.on_value do |event|
|
73
|
+
puts "I got a #{event}!"
|
74
|
+
end
|
75
|
+
```
|
76
|
+
|
77
|
+
You can combine two streams together:
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
merged_stream = Frappuccino::Stream.merge(stream_one, stream_two)
|
81
|
+
|
82
|
+
# or
|
83
|
+
|
84
|
+
merged_stream = Frappuccino::Stream.new(button_one, button_two)
|
85
|
+
```
|
86
|
+
|
87
|
+
You can select events from a stream, too:
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
stream = Frappuccino::Stream.new(button, something_else)
|
91
|
+
filtered_stream = stream.select{|event| event == :pushed }
|
92
|
+
|
93
|
+
filtered_stream.on_value do |event|
|
94
|
+
# event will only ever be :pushed
|
95
|
+
end
|
96
|
+
```
|
97
|
+
|
98
|
+
## Contributing
|
99
|
+
|
100
|
+
1. Fork it
|
101
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
102
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
103
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
104
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/frappuccino.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'frappuccino/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "frappuccino"
|
8
|
+
spec.version = Frappuccino::VERSION
|
9
|
+
spec.authors = ["Steve Klabnik"]
|
10
|
+
spec.email = ["steve@steveklabnik.com"]
|
11
|
+
spec.description = %q{A library to do Functional Reactive Programming in Ruby.}
|
12
|
+
spec.summary = %q{Functional Reactive Programming in Ruby.}
|
13
|
+
spec.homepage = "https://github.com/steveklabnik/frappuccino"
|
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_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
end
|
data/lib/frappuccino.rb
CHANGED
@@ -1,140 +1,3 @@
|
|
1
|
-
require "
|
2
|
-
require "
|
3
|
-
|
4
|
-
attr_writer :files, :documentations, :doctree, :title
|
5
|
-
def initialize title, files
|
6
|
-
if File.directory? files
|
7
|
-
docfiles = []
|
8
|
-
Dir.glob("#{files}/*").each do |file|
|
9
|
-
docfiles.push(file) unless File.directory? file
|
10
|
-
end
|
11
|
-
files = docfiles
|
12
|
-
end
|
13
|
-
@files = files
|
14
|
-
@documentations = []
|
15
|
-
@doctree = {:namespaces => {}}
|
16
|
-
@title = title
|
17
|
-
iterate_files
|
18
|
-
documentations_to_doctree
|
19
|
-
# sort alphabetically
|
20
|
-
@doctree[:namespaces] = @doctree[:namespaces].sort_by {|k,v| k}
|
21
|
-
generate_html
|
22
|
-
end
|
23
|
-
|
24
|
-
def iterate_files
|
25
|
-
for file in @files
|
26
|
-
File.open(file) do |opened_file|
|
27
|
-
puts "* Parsing documentation from #{file}"
|
28
|
-
collect_documentation opened_file
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
# parses documentation bodies
|
34
|
-
def collect_documentation opened_file
|
35
|
-
doc_start = false
|
36
|
-
current_documentation = []
|
37
|
-
opened_file.each_line do |line|
|
38
|
-
# documentation starts here
|
39
|
-
if line.include?("###") and !doc_start
|
40
|
-
doc_start = true
|
41
|
-
# documentation ends here
|
42
|
-
elsif line.include?("###") and doc_start
|
43
|
-
@documentations.push current_documentation
|
44
|
-
current_documentation = []
|
45
|
-
doc_start = false
|
46
|
-
# in the middle of documentation
|
47
|
-
elsif doc_start
|
48
|
-
current_documentation.push line
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
def documentations_to_doctree
|
54
|
-
@documentations.each do |docbody|
|
55
|
-
doctree_format docbody
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
# transforms array of strings to doctree hash
|
60
|
-
def doctree_format docbody
|
61
|
-
doctree = {}
|
62
|
-
value_regexp = /^.*:\s(.*)/
|
63
|
-
namespace_arr = []
|
64
|
-
namespace_tree = {}
|
65
|
-
function_name = nil
|
66
|
-
object_name = nil
|
67
|
-
description = ""
|
68
|
-
rest_is_description = false
|
69
|
-
|
70
|
-
docbody.each do |line|
|
71
|
-
if line.match(/^\s*function/)
|
72
|
-
function_name = line.match(value_regexp)[1]
|
73
|
-
elsif line.match(/^\s*object/)
|
74
|
-
object_name = line.match(value_regexp)[1]
|
75
|
-
elsif line.match(/^\s*description/)
|
76
|
-
rest_is_description = true
|
77
|
-
elsif line.match(/^\s*namespace/)
|
78
|
-
namespace_arr = line.match(value_regexp)[1].split(".")
|
79
|
-
elsif rest_is_description
|
80
|
-
description += line
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
|
-
full_namespace = namespace_arr.join(".")
|
85
|
-
|
86
|
-
|
87
|
-
if !function_name.nil?
|
88
|
-
docdata = {:full_namespace => full_namespace, :functions => [{:function_name => function_name, :description => description}]}
|
89
|
-
elsif !object_name.nil?
|
90
|
-
docdata = {:full_namespace => full_namespace, :objects => [{:object_name => object_name, :description => description}]}
|
91
|
-
else
|
92
|
-
if rest_is_description
|
93
|
-
docdata = {:full_namespace => full_namespace, :description => description}
|
94
|
-
else
|
95
|
-
docdata = {:full_namespace => full_namespace}
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
namespace_is_added = false
|
100
|
-
@doctree[:namespaces].each do |key, value|
|
101
|
-
namespace_is_added = true if key == full_namespace
|
102
|
-
end
|
103
|
-
|
104
|
-
if namespace_is_added
|
105
|
-
@doctree[:namespaces][full_namespace] = deep_safe_merge(@doctree[:namespaces][full_namespace], docdata)
|
106
|
-
else
|
107
|
-
@doctree[:namespaces][full_namespace] = docdata
|
108
|
-
end
|
109
|
-
|
110
|
-
end
|
111
|
-
|
112
|
-
|
113
|
-
def generate_html
|
114
|
-
template = ERB.new File.new("#{File.dirname(__FILE__)}/../template/index.erb").read
|
115
|
-
rendered_template = template.result(binding)
|
116
|
-
FileUtils.mkdir "docs" unless File.directory? "docs"
|
117
|
-
File.open("docs/index.html", "w") do |f|
|
118
|
-
f.write rendered_template
|
119
|
-
end
|
120
|
-
FileUtils.cp("#{File.dirname(__FILE__)}/../template/bg.png", "docs/bg.png")
|
121
|
-
puts "* Documentation file generated (docs/index.html)"
|
122
|
-
end
|
123
|
-
|
124
|
-
# helpers:
|
125
|
-
|
126
|
-
def deep_safe_merge(source_hash, new_hash)
|
127
|
-
source_hash.merge(new_hash) do |key, old, new|
|
128
|
-
if new.respond_to?(:blank) && new.blank?
|
129
|
-
old
|
130
|
-
elsif (old.kind_of?(Hash) and new.kind_of?(Hash))
|
131
|
-
deep_merge(old, new)
|
132
|
-
elsif (old.kind_of?(Array) and new.kind_of?(Array))
|
133
|
-
old.concat(new).uniq
|
134
|
-
else
|
135
|
-
new
|
136
|
-
end
|
137
|
-
end
|
138
|
-
end
|
139
|
-
|
140
|
-
end
|
1
|
+
require "frappuccino/version"
|
2
|
+
require "frappuccino/stream"
|
3
|
+
require "frappuccino/property"
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
# before we require all of the subclasses, we need to have Stream defined
|
4
|
+
module Frappuccino
|
5
|
+
class Stream
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'frappuccino/source'
|
10
|
+
|
11
|
+
require 'frappuccino/stream/map'
|
12
|
+
require 'frappuccino/stream/select'
|
13
|
+
require 'frappuccino/stream/zip'
|
14
|
+
require 'frappuccino/stream/drop'
|
15
|
+
require 'frappuccino/stream/scan'
|
16
|
+
require 'frappuccino/stream/take'
|
17
|
+
|
18
|
+
def not_implemented(m, message)
|
19
|
+
define_method m do |*args, &blk|
|
20
|
+
raise NotImplementedError, "##{m} is not supported, because #{message}."
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module Frappuccino
|
25
|
+
class Stream
|
26
|
+
include Observable
|
27
|
+
|
28
|
+
def initialize(*sources)
|
29
|
+
sources.each do |source|
|
30
|
+
source.extend(Frappuccino::Source).add_observer(self)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def update(event)
|
35
|
+
occur(event)
|
36
|
+
end
|
37
|
+
|
38
|
+
def count(*args, &blk)
|
39
|
+
stream = if args.count > 0
|
40
|
+
self.select { |value| value == args.first }
|
41
|
+
elsif blk
|
42
|
+
self.select { |value| blk.call(value) }
|
43
|
+
else
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
Property.new(0, stream.scan(0) { |last| last + 1 })
|
48
|
+
end
|
49
|
+
|
50
|
+
not_implemented(:cycle, "it relies on having a end to the Enumerable")
|
51
|
+
not_implemented(:all?, "it needs a stream that terminates.")
|
52
|
+
not_implemented(:chunk, "it needs a stream that terminates.")
|
53
|
+
not_implemented(:any?, "it could resolve to ⊥. You probably want #select")
|
54
|
+
not_implemented(:find, "it could resolve to ⊥. You probably want #select")
|
55
|
+
|
56
|
+
alias :detect :find
|
57
|
+
|
58
|
+
def map(hash = nil, &blk)
|
59
|
+
blk = lambda { |event| hash.fetch(event) { hash[:default] } } if hash
|
60
|
+
Map.new(self, &blk)
|
61
|
+
end
|
62
|
+
alias :collect :map
|
63
|
+
alias :map_stream :map
|
64
|
+
|
65
|
+
def drop(n)
|
66
|
+
Drop.new(self, n)
|
67
|
+
end
|
68
|
+
|
69
|
+
def take(n)
|
70
|
+
Take.new(self, n)
|
71
|
+
end
|
72
|
+
|
73
|
+
def inject(start, &blk)
|
74
|
+
Property.new(start, self.scan(start, &blk))
|
75
|
+
end
|
76
|
+
|
77
|
+
def select(&blk)
|
78
|
+
Select.new(self, &blk)
|
79
|
+
end
|
80
|
+
|
81
|
+
def zip(stream)
|
82
|
+
Zip.new(self, stream)
|
83
|
+
end
|
84
|
+
|
85
|
+
def scan(zero, &blk)
|
86
|
+
Scan.new(self, zero, &blk)
|
87
|
+
end
|
88
|
+
|
89
|
+
def on_value(&blk)
|
90
|
+
callbacks << blk
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.merge(stream_one, stream_two)
|
94
|
+
new(stream_one, stream_two)
|
95
|
+
end
|
96
|
+
|
97
|
+
protected
|
98
|
+
|
99
|
+
def occur(value)
|
100
|
+
callbacks.each do |callback|
|
101
|
+
callback.call(value)
|
102
|
+
end
|
103
|
+
|
104
|
+
changed
|
105
|
+
notify_observers(value)
|
106
|
+
end
|
107
|
+
|
108
|
+
def callbacks
|
109
|
+
@callbacks ||= []
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|