plumb 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -0
- data/README.md +291 -19
- data/examples/command_objects.rb +207 -0
- data/examples/concurrent_downloads.rb +107 -0
- data/examples/csv_stream.rb +97 -0
- data/examples/programmers.csv +201 -0
- data/examples/weekdays.rb +66 -0
- data/lib/plumb/array_class.rb +25 -19
- data/lib/plumb/build.rb +3 -0
- data/lib/plumb/hash_class.rb +44 -13
- data/lib/plumb/hash_map.rb +34 -0
- data/lib/plumb/interface_class.rb +6 -4
- data/lib/plumb/json_schema_visitor.rb +117 -74
- data/lib/plumb/match_class.rb +8 -5
- data/lib/plumb/metadata.rb +3 -0
- data/lib/plumb/metadata_visitor.rb +45 -40
- data/lib/plumb/rules.rb +6 -7
- data/lib/plumb/schema.rb +37 -41
- data/lib/plumb/static_class.rb +4 -4
- data/lib/plumb/step.rb +6 -1
- data/lib/plumb/steppable.rb +36 -34
- data/lib/plumb/stream_class.rb +61 -0
- data/lib/plumb/tagged_hash.rb +12 -3
- data/lib/plumb/transform.rb +6 -1
- data/lib/plumb/tuple_class.rb +8 -5
- data/lib/plumb/types.rb +19 -60
- data/lib/plumb/value_class.rb +5 -2
- data/lib/plumb/version.rb +1 -1
- data/lib/plumb/visitor_handlers.rb +13 -9
- data/lib/plumb.rb +1 -0
- metadata +8 -2
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler'
|
4
|
+
Bundler.setup(:examples)
|
5
|
+
require 'plumb'
|
6
|
+
require 'open-uri'
|
7
|
+
require 'fileutils'
|
8
|
+
require 'digest/md5'
|
9
|
+
|
10
|
+
# Mixin built-in Plumb types, and provide a namespace for core types and
|
11
|
+
# pipelines in this example.
|
12
|
+
module Types
|
13
|
+
include Plumb::Types
|
14
|
+
|
15
|
+
# Turn a string into an URI
|
16
|
+
URL = String[/^https?:/].build(::URI, :parse)
|
17
|
+
|
18
|
+
# a Struct to holw image data
|
19
|
+
Image = Data.define(:url, :io)
|
20
|
+
|
21
|
+
# A (naive) step to download files from the internet
|
22
|
+
# and return an Image struct.
|
23
|
+
# It implements the #call(Result) => Result interface.
|
24
|
+
# required by all Plumb steps.
|
25
|
+
# URI => Image
|
26
|
+
Download = Plumb::Step.new do |result|
|
27
|
+
io = URI.open(result.value)
|
28
|
+
result.valid(Image.new(result.value.to_s, io))
|
29
|
+
end
|
30
|
+
|
31
|
+
# A configurable file-system cache to read and write files from.
|
32
|
+
class Cache
|
33
|
+
def initialize(dir = '.')
|
34
|
+
@dir = dir
|
35
|
+
FileUtils.mkdir_p(dir)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Wrap the #reader and #wruter methods into Plumb steps
|
39
|
+
# A step only needs #call(Result) => Result to work in a pipeline,
|
40
|
+
# but wrapping it in Plumb::Step provides the #>> and #| methods for composability,
|
41
|
+
# as well as all the other helper methods provided by the Steppable module.
|
42
|
+
def read = Plumb::Step.new(method(:reader))
|
43
|
+
def write = Plumb::Step.new(method(:writer))
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
# URL => Image
|
48
|
+
def reader(result)
|
49
|
+
path = path_for(result.value)
|
50
|
+
return result.invalid(errors: "file #{path} does not exist") unless File.exist?(path)
|
51
|
+
|
52
|
+
result.valid Types::Image.new(url: path, io: File.new(path))
|
53
|
+
end
|
54
|
+
|
55
|
+
# Image => Image
|
56
|
+
def writer(result)
|
57
|
+
image = result.value
|
58
|
+
path = path_for(image.url)
|
59
|
+
File.open(path, 'wb') { |f| f.write(image.io.read) }
|
60
|
+
result.valid image.with(url: path, io: File.new(path))
|
61
|
+
end
|
62
|
+
|
63
|
+
def path_for(url)
|
64
|
+
url = url.to_s
|
65
|
+
ext = File.extname(url)
|
66
|
+
name = [Digest::MD5.hexdigest(url), ext].compact.join
|
67
|
+
File.join(@dir, name)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
###################################
|
73
|
+
# Program 1: idempoent download of images from the internet
|
74
|
+
# If not present in the cache, images are downloaded and written to the cache.
|
75
|
+
# Otherwise images are listed directly from the cache (files on disk).
|
76
|
+
###################################
|
77
|
+
|
78
|
+
cache = Types::Cache.new('./examples/data/downloads')
|
79
|
+
|
80
|
+
# A pipeline representing a single image download.
|
81
|
+
# 1). Take a valid URL string.
|
82
|
+
# 2). Attempt reading the file from the cache. Return that if it exists.
|
83
|
+
# 3). Otherwise, download the file from the internet and write it to the cache.
|
84
|
+
IdempotentDownload = Types::URL >> (cache.read | (Types::Download >> cache.write))
|
85
|
+
|
86
|
+
# An array of downloadable images,
|
87
|
+
# marked as concurrent so that all IO operations are run in threads.
|
88
|
+
Images = Types::Array[IdempotentDownload].concurrent
|
89
|
+
|
90
|
+
urls = [
|
91
|
+
'https://as1.ftcdn.net/v2/jpg/07/67/24/52/1000_F_767245234_NdiDr9LOkypOEKtXiDDoM1m42zBQ0hZe.jpg',
|
92
|
+
'https://as1.ftcdn.net/v2/jpg/07/83/02/00/1000_F_783020069_HaP9UCZs2UXUnKxpGHDoddt0vuX4vU9U.jpg',
|
93
|
+
'https://as2.ftcdn.net/v2/jpg/07/32/27/53/1000_F_732275398_r2t1cnxSXGUkZSgxtqhg40UupKiqcywJ.jpg',
|
94
|
+
'https://as1.ftcdn.net/v2/jpg/07/46/41/18/1000_F_746411866_WwQBojO7xMeVFTua2BuEZdKGDI2vsgAH.jpg',
|
95
|
+
'https://as2.ftcdn.net/v2/jpg/07/43/50/53/1000_F_743505311_MJ3zo09rH7rUvHrCKlBotojm6GLw3SCT.jpg',
|
96
|
+
'https://images.pexels.com/photos/346529/pexels-photo-346529.jpeg'
|
97
|
+
]
|
98
|
+
|
99
|
+
# raise CachedDownload.parse(url).inspect
|
100
|
+
# raise CachedDownload.parse(urls.first).inspect
|
101
|
+
# Run the program. The images are downloaded and written to the ./downloads directory.
|
102
|
+
# Running this multiple times will only download the images once, and list them from the cache.
|
103
|
+
# You can try deleting all or some of the files and re-running.
|
104
|
+
Images.resolve(urls).tap do |result|
|
105
|
+
puts result.valid? ? 'valid!' : result.errors
|
106
|
+
result.value.each { |img| p img }
|
107
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'plumb'
|
5
|
+
require 'csv'
|
6
|
+
|
7
|
+
# Defines types and pipelines for opening and working with CSV streams.
|
8
|
+
# Run with `bundle exec ruby examples/csv_stream.rb`
|
9
|
+
module Types
|
10
|
+
include Plumb::Types
|
11
|
+
|
12
|
+
# Open a File
|
13
|
+
# ex. file = FileStep.parse('./files/data.csv') # => File
|
14
|
+
OpenFile = String
|
15
|
+
.check('no file for that path') { |s| ::File.exist?(s) }
|
16
|
+
.build(::File)
|
17
|
+
|
18
|
+
# Turn a File into a CSV stream
|
19
|
+
# ex. csv_enum = FileToCSV.parse(file) #=> Enumerator
|
20
|
+
FileToCSV = Types::Any[::File]
|
21
|
+
.build(CSV)
|
22
|
+
.transform(::Enumerator, &:each)
|
23
|
+
|
24
|
+
# Turn a string file path into a CSV stream
|
25
|
+
# ex. csv_enum = StrinToCSV.parse('./files/data.csv') #=> Enumerator
|
26
|
+
StringToCSV = OpenFile >> FileToCSV
|
27
|
+
end
|
28
|
+
|
29
|
+
#################################################
|
30
|
+
# Program 1: stream a CSV list of programmers and filter them by age.
|
31
|
+
#################################################
|
32
|
+
|
33
|
+
# This is a CSV row for a programmer over the age of 18.
|
34
|
+
AdultProgrammer = Types::Tuple[
|
35
|
+
# Name
|
36
|
+
String,
|
37
|
+
# Age. Coerce to Integer and constrain to 18 or older.
|
38
|
+
Types::Lax::Integer[18..],
|
39
|
+
# Programming language
|
40
|
+
String
|
41
|
+
]
|
42
|
+
|
43
|
+
# An Array of AdultProgrammer.
|
44
|
+
AdultProgrammerArray = Types::Array[AdultProgrammer]
|
45
|
+
|
46
|
+
# A pipeline to open a file, parse CSV and stream rows of AdultProgrammer.
|
47
|
+
AdultProgrammerStream = Types::StringToCSV >> AdultProgrammerArray.stream
|
48
|
+
|
49
|
+
# List adult programmers from file.
|
50
|
+
puts 'Adult programmers:'
|
51
|
+
AdultProgrammerStream.parse('./examples/programmers.csv').each do |row|
|
52
|
+
puts row.value.inspect if row.valid?
|
53
|
+
end
|
54
|
+
|
55
|
+
# The filtering can also be achieved with Stream#filter
|
56
|
+
# AdultProgrammerStream = Types::StringToCSV >> AdultProgrammerArray.stream.filtered
|
57
|
+
|
58
|
+
#################################################
|
59
|
+
# Program 2: list Ruby programmers from a CSV file.
|
60
|
+
#################################################
|
61
|
+
|
62
|
+
RubyProgrammer = Types::Tuple[
|
63
|
+
String, # Name
|
64
|
+
Types::Lax::Integer, # Age
|
65
|
+
Types::String[/^ruby$/i] # Programming language
|
66
|
+
]
|
67
|
+
|
68
|
+
# A pipeline to open a file, parse CSV and stream rows of AdultProgrammer.
|
69
|
+
# This time we use Types::Stream directly.
|
70
|
+
RubyProgrammerStream = Types::StringToCSV >> Types::Stream[RubyProgrammer].filtered
|
71
|
+
|
72
|
+
# List Ruby programmers from file.
|
73
|
+
puts
|
74
|
+
puts '----------------------------------------'
|
75
|
+
puts 'Ruby programmers:'
|
76
|
+
RubyProgrammerStream.parse('./examples/programmers.csv').each do |person|
|
77
|
+
puts person.inspect
|
78
|
+
end
|
79
|
+
|
80
|
+
# We can filter Ruby OR Elixir programmers with a union type.
|
81
|
+
# Lang = Types::String['ruby'] | Types::String['elixir']
|
82
|
+
# Or with allowe values:
|
83
|
+
# Lang = Types::String.options(%w[ruby elixir])
|
84
|
+
|
85
|
+
#################################################
|
86
|
+
# Program 3: negate the stream above to list non-Ruby programmers.
|
87
|
+
#################################################
|
88
|
+
|
89
|
+
# See the `.not` which negates the type.
|
90
|
+
NonRubyProgrammerStream = Types::StringToCSV >> Types::Stream[RubyProgrammer.not].filtered
|
91
|
+
|
92
|
+
puts
|
93
|
+
puts '----------------------------------------'
|
94
|
+
puts 'NON Ruby programmers:'
|
95
|
+
NonRubyProgrammerStream.parse('./examples/programmers.csv').each do |person|
|
96
|
+
puts person.inspect
|
97
|
+
end
|
@@ -0,0 +1,201 @@
|
|
1
|
+
name,age,language
|
2
|
+
Krysten Tromp,57,C++
|
3
|
+
Irena Mayert III,21,ruby
|
4
|
+
Lucile Jacobi I,28,lisp
|
5
|
+
Warner Ullrich DC,64,rust
|
6
|
+
Refugio Cronin,13,F#
|
7
|
+
Emery Kris,84,ocaml
|
8
|
+
Sherise Wisoky,73,elm
|
9
|
+
Bridget DuBuque,75,elixir
|
10
|
+
Maynard Lubowitz V,58,C++
|
11
|
+
Rosamaria Heidenreich,61,fortran
|
12
|
+
Marla Fritsch,70,golang
|
13
|
+
Williams Little MD,48,golang
|
14
|
+
Bernard Wilderman,25,ocaml
|
15
|
+
Sen. Hiram Bayer,37,gleam
|
16
|
+
Ginger Mueller,60,elixir
|
17
|
+
Karyl Hegmann,63,ruby
|
18
|
+
Terrilyn Fadel,40,ocaml
|
19
|
+
Edward Abshire,14,gleam
|
20
|
+
Clyde Lesch,67,elixir
|
21
|
+
Ms. Loretta Botsford,40,ruby
|
22
|
+
Sen. Susann Gleichner,19,zig
|
23
|
+
Von Thompson,80,javascript
|
24
|
+
Carrol Kirlin II,43,ocaml
|
25
|
+
Samual Welch,17,elm
|
26
|
+
Samara Hegmann II,50,fortran
|
27
|
+
Tyron Bogan,65,rust
|
28
|
+
Juan Quitzon,57,zig
|
29
|
+
Bev Feest CPA,15,javascript
|
30
|
+
Walter Parisian,17,golang
|
31
|
+
The Hon. Zachary Funk,55,ruby
|
32
|
+
Jaimie Stoltenberg,77,rust
|
33
|
+
Jarrod Turcotte DC,68,elixir
|
34
|
+
Dr. Jason Jacobs,40,zig
|
35
|
+
Miss Neal Leffler,17,rust
|
36
|
+
Wilfredo Kemmer,30,C++
|
37
|
+
Fr. Leon Jerde,35,ruby
|
38
|
+
Mr. Ray Anderson,31,C
|
39
|
+
Jed Skiles,63,lua
|
40
|
+
Delena Raynor LLD,40,ruby
|
41
|
+
Pres. Dirk Kunde,74,rust
|
42
|
+
Franklyn Kuhic,55,ruby
|
43
|
+
Jon Deckow,68,golang
|
44
|
+
Anglea Beier LLD,17,C++
|
45
|
+
Jene Labadie,60,lisp
|
46
|
+
Ezequiel Wyman,50,F#
|
47
|
+
Jessie Medhurst,33,F#
|
48
|
+
Luke Bergnaum,57,ruby
|
49
|
+
Douglass Goodwin,83,zig
|
50
|
+
Garry Farrell,67,ocaml
|
51
|
+
Warren Harber III,23,rust
|
52
|
+
Britni Mohr,60,elixir
|
53
|
+
Terra Pouros DDS,60,F#
|
54
|
+
Pia Bernhard,37,fortran
|
55
|
+
Akiko Hoppe,14,ocaml
|
56
|
+
Samuel Muller,56,rust
|
57
|
+
Esperanza Kutch,67,C++
|
58
|
+
Hal Gislason,75,ruby
|
59
|
+
Prof. Jonell Turcotte,61,ruby
|
60
|
+
Lindsey Hermiston,29,ruby
|
61
|
+
Deadra Muller,51,fortran
|
62
|
+
Amb. Fern Crist,58,elm
|
63
|
+
Enola Grant,39,zig
|
64
|
+
Mickey Lueilwitz DVM,80,lua
|
65
|
+
Raymon Jacobi,34,golang
|
66
|
+
Jody Rowe V,27,golang
|
67
|
+
Lilliam Hyatt,36,zig
|
68
|
+
Fr. Larraine Klein,22,javascript
|
69
|
+
Derick Rutherford DC,41,C
|
70
|
+
Eugene Kiehn DVM,20,C
|
71
|
+
Dianna Kunze DDS,22,fortran
|
72
|
+
Denis Reilly,67,zig
|
73
|
+
Alejandrina Waelchi,70,rust
|
74
|
+
Odell Cassin,20,ocaml
|
75
|
+
Travis Daugherty,77,F#
|
76
|
+
Guillermo McCullough,15,ruby
|
77
|
+
Malena Nitzsche,12,lisp
|
78
|
+
Jess Gleason,61,ruby
|
79
|
+
Mr. Kenton Quitzon,77,C++
|
80
|
+
Sixta Torp,36,ruby
|
81
|
+
Teddy McClure,15,ruby
|
82
|
+
Alexandra Towne CPA,73,elixir
|
83
|
+
Harrison Hartmann CPA,66,lua
|
84
|
+
Gretta Kirlin Sr.,17,rust
|
85
|
+
Rev. Jordan Yost,36,ruby
|
86
|
+
Geraldo Yundt LLD,17,elixir
|
87
|
+
Emelia Pouros,31,fortran
|
88
|
+
Msgr. Coletta Crist,43,zig
|
89
|
+
Vernice Pfannerstill,31,zig
|
90
|
+
Rev. Marvel Mayer,55,ruby
|
91
|
+
Amb. Chanelle Ruecker,14,elixir
|
92
|
+
Sallie Roob PhD,75,gleam
|
93
|
+
Enoch Funk,33,golang
|
94
|
+
Alex Reichel,85,F#
|
95
|
+
Laure Boehm,24,julia
|
96
|
+
Valeri Dietrich,72,ruby
|
97
|
+
Tabatha Kerluke,58,julia
|
98
|
+
Bert Sauer,73,gleam
|
99
|
+
Morgan Heaney,39,julia
|
100
|
+
Lora Hackett,74,javascript
|
101
|
+
Rona Leuschke,72,rust
|
102
|
+
Mora Bode,19,elm
|
103
|
+
Daniell Hegmann,37,golang
|
104
|
+
Richie Waters CPA,23,javascript
|
105
|
+
Katharine Huel,36,C
|
106
|
+
Lauren Abbott,37,C
|
107
|
+
Daisey Cremin,62,ruby
|
108
|
+
Evelyn Koch,49,F#
|
109
|
+
Pres. Cassey Hammes,82,gleam
|
110
|
+
Msgr. Wilhelmina Bauch,18,C
|
111
|
+
Hilaria Emmerich,35,rust
|
112
|
+
Rev. Julieann Hickle,18,C
|
113
|
+
Pres. Corey Shields,14,ruby
|
114
|
+
Donetta Lehner PhD,34,ruby
|
115
|
+
Cyndi Schumm,83,ruby
|
116
|
+
Newton West,52,elm
|
117
|
+
Carleen Wisozk,42,F#
|
118
|
+
Alysha Marvin Ret.,35,lisp
|
119
|
+
Alex Balistreri,83,javascript
|
120
|
+
Zulma Parker PhD,45,ocaml
|
121
|
+
Reyes Bogisich Sr.,70,ocaml
|
122
|
+
Dario Batz,38,elm
|
123
|
+
Jacquelyn MacGyver,59,gleam
|
124
|
+
Gov. Zachary Schuster,64,ruby
|
125
|
+
Leonard Braun,65,ruby
|
126
|
+
Ivette Schuppe,30,ruby
|
127
|
+
Adrian Parisian,45,elm
|
128
|
+
Dr. Young Rohan,20,fortran
|
129
|
+
Dong Hansen,77,F#
|
130
|
+
Johnny Hammes,44,F#
|
131
|
+
Amb. Terence Cummerata,49,ruby
|
132
|
+
Taylor Nitzsche,40,F#
|
133
|
+
Mr. Bennett Price,46,C
|
134
|
+
Genaro Blick VM,23,fortran
|
135
|
+
Ashley Osinski,71,ruby
|
136
|
+
Jae D'Amore,50,elm
|
137
|
+
Katherine Ward,63,lisp
|
138
|
+
Anderson O'Conner,70,ocaml
|
139
|
+
Amb. Chrystal Schmitt,70,rust
|
140
|
+
Vaughn Larkin,52,F#
|
141
|
+
Jada Brown Jr.,53,golang
|
142
|
+
Pauletta Krajcik,72,gleam
|
143
|
+
Cinthia Morar,26,ruby
|
144
|
+
Loyce Windler,51,julia
|
145
|
+
Ms. Sharee Walker,84,ruby
|
146
|
+
Raymundo Kohler,14,fortran
|
147
|
+
Natalya Towne Sr.,72,C++
|
148
|
+
Rayford Kemmer,12,golang
|
149
|
+
Rev. Tisa Harber,44,elm
|
150
|
+
Jorge Schaden,72,lua
|
151
|
+
Pam Marquardt,12,lua
|
152
|
+
Xenia Wisoky,13,javascript
|
153
|
+
Audrea Purdy,75,lua
|
154
|
+
Joesph Russel DC,18,javascript
|
155
|
+
Marty Ratke III,38,rust
|
156
|
+
Jordon Bradtke,25,C
|
157
|
+
Titus Cronin Esq.,36,ruby
|
158
|
+
Ms. Lucio Vandervort,64,golang
|
159
|
+
Theo Nicolas,50,julia
|
160
|
+
Hazel Pouros,29,ruby
|
161
|
+
Rey Lubowitz,59,elixir
|
162
|
+
Ike Hessel,66,C
|
163
|
+
Alec Wilderman,42,ruby
|
164
|
+
Leesa Ledner,71,lisp
|
165
|
+
Jeneva Gutkowski,33,lua
|
166
|
+
Isaias MacGyver,32,F#
|
167
|
+
Daryl Senger I,36,ruby
|
168
|
+
Mr. Gonzalo Breitenberg,50,julia
|
169
|
+
Aretha Halvorson,17,ocaml
|
170
|
+
Ralph Wisoky,15,F#
|
171
|
+
Demarcus Stehr,80,javascript
|
172
|
+
Sheldon Lemke,47,golang
|
173
|
+
Kimiko Hermiston,20,golang
|
174
|
+
Manda Pfannerstill,20,F#
|
175
|
+
Haywood Rowe,85,F#
|
176
|
+
Roxanne Gerhold,29,ruby
|
177
|
+
Chanelle Beatty,70,ruby
|
178
|
+
Shirlee Hoeger,18,ruby
|
179
|
+
Beula Denesik,68,C++
|
180
|
+
Roberto Lang,59,gleam
|
181
|
+
Marcy Rau,77,javascript
|
182
|
+
Samira Hilll,51,F#
|
183
|
+
Clarissa Simonis,82,javascript
|
184
|
+
Elijah Cole,57,ocaml
|
185
|
+
Freida Wilkinson,34,lua
|
186
|
+
Polly Wehner,82,C++
|
187
|
+
Lindsey Ullrich,80,elixir
|
188
|
+
Freddie Auer,42,ruby
|
189
|
+
Luz Zemlak,37,lisp
|
190
|
+
Colin Emard,74,ruby
|
191
|
+
Dion Hudson,56,gleam
|
192
|
+
Daisy Crist,12,lua
|
193
|
+
Keturah Ortiz,64,ruby
|
194
|
+
Everette Shanahan Sr.,28,zig
|
195
|
+
Prof. Beckie Wiza,80,ruby
|
196
|
+
Msgr. Annabelle Howe,35,ocaml
|
197
|
+
Jeremy Effertz,22,julia
|
198
|
+
Dinorah Graham,38,elixir
|
199
|
+
Kate Bernier,35,ruby
|
200
|
+
Jerrold Ortiz,22,ruby
|
201
|
+
Bart Moen,74,ruby
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler'
|
4
|
+
Bundler.setup(:examples)
|
5
|
+
require 'plumb'
|
6
|
+
|
7
|
+
# bundle exec examples/weekdays.rb
|
8
|
+
#
|
9
|
+
# Data types to represent and parse an array of days of the week.
|
10
|
+
# Input data can be an array of day names or numbers, ex.
|
11
|
+
# ['monday', 'tuesday', 'wednesday']
|
12
|
+
# [1, 2, 3]
|
13
|
+
#
|
14
|
+
# Or mixed:
|
15
|
+
# [1, 'Tuesday', 3]
|
16
|
+
#
|
17
|
+
# Validate that there aren't repeated days, ex. [1, 2, 4, 2]
|
18
|
+
# The output is an array of day numbers, ex. [1, 2, 3]
|
19
|
+
module Types
|
20
|
+
include Plumb::Types
|
21
|
+
|
22
|
+
DAYS = {
|
23
|
+
'monday' => 1,
|
24
|
+
'tuesday' => 2,
|
25
|
+
'wednesday' => 3,
|
26
|
+
'thursday' => 4,
|
27
|
+
'friday' => 5,
|
28
|
+
'saturday' => 6,
|
29
|
+
'sunday' => 7
|
30
|
+
}.freeze
|
31
|
+
|
32
|
+
# Validate that a string is a valid day name, down-case it.
|
33
|
+
DayName = String
|
34
|
+
.transform(::String, &:downcase)
|
35
|
+
.options(DAYS.keys)
|
36
|
+
|
37
|
+
# Turn a day name into its number, or validate that a number is a valid day number.
|
38
|
+
DayNameOrNumber = DayName.transform(::Integer) { |v| DAYS[v] } | Integer.options(DAYS.values)
|
39
|
+
|
40
|
+
# An Array for days of the week, with no repeated days.
|
41
|
+
# Ex. [1, 2, 3, 4, 5, 6, 7], [1, 2, 4], ['monday', 'tuesday', 'wednesday', 7]
|
42
|
+
# Turn day names into numbers, and sort the array.
|
43
|
+
Week = Array[DayNameOrNumber]
|
44
|
+
.rule(size: 1..7)
|
45
|
+
.check('repeated days') { |days| days.uniq.size == days.size }
|
46
|
+
.transform(::Array, &:sort)
|
47
|
+
end
|
48
|
+
|
49
|
+
p Types::DayNameOrNumber.parse('monday') # => 1
|
50
|
+
p Types::DayNameOrNumber.parse(3) # => 3
|
51
|
+
p Types::DayName.parse('TueSday') # => "tuesday
|
52
|
+
p Types::Week.parse([3, 2, 1, 4, 5, 6, 7]) # => [1, 2, 3, 4, 5, 6, 7]
|
53
|
+
p Types::Week.parse([1, 'Tuesday', 3, 4, 5, 'saturday', 7]) # => [1, 2, 3, 4, 5, 6, 7]
|
54
|
+
|
55
|
+
# p Types::Week[[1, 1, 3, 4, 5, 6, 7]] # raises Plumb::TypeError: repeated days
|
56
|
+
#
|
57
|
+
# Or use these types as part of other composite types, ex.
|
58
|
+
#
|
59
|
+
# PartTimeJob = Types::Hash[
|
60
|
+
# role: Types::String.present,
|
61
|
+
# days_of_the_week: Types::Week
|
62
|
+
# ]
|
63
|
+
#
|
64
|
+
# result = PartTimeJob.resolve(role: 'Ruby dev', days_of_the_week: %w[Tuesday Wednesday])
|
65
|
+
# result.valid? # true
|
66
|
+
# result.value # { role: 'Ruby dev', days_of_the_week: [2, 3] }
|
data/lib/plumb/array_class.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
require 'concurrent'
|
4
4
|
require 'plumb/steppable'
|
5
5
|
require 'plumb/result'
|
6
|
-
require 'plumb/
|
6
|
+
require 'plumb/stream_class'
|
7
7
|
|
8
8
|
module Plumb
|
9
9
|
class ArrayClass
|
@@ -12,15 +12,7 @@ module Plumb
|
|
12
12
|
attr_reader :element_type
|
13
13
|
|
14
14
|
def initialize(element_type: Types::Any)
|
15
|
-
@element_type =
|
16
|
-
when Steppable
|
17
|
-
element_type
|
18
|
-
when ::Hash
|
19
|
-
HashClass.new(element_type)
|
20
|
-
else
|
21
|
-
raise ArgumentError,
|
22
|
-
"element_type #{element_type.inspect} must be a Steppable"
|
23
|
-
end
|
15
|
+
@element_type = Steppable.wrap(element_type)
|
24
16
|
|
25
17
|
freeze
|
26
18
|
end
|
@@ -35,12 +27,22 @@ module Plumb
|
|
35
27
|
ConcurrentArrayClass.new(element_type:)
|
36
28
|
end
|
37
29
|
|
38
|
-
|
39
|
-
|
30
|
+
def stream
|
31
|
+
StreamClass.new(element_type:)
|
32
|
+
end
|
33
|
+
|
34
|
+
def filtered
|
35
|
+
MatchClass.new(::Array) >> Step.new(nil, "Array[#{element_type}].filtered") do |result|
|
36
|
+
arr = result.value.each.with_object([]) do |e, memo|
|
37
|
+
r = element_type.resolve(e)
|
38
|
+
memo << r.value if r.valid?
|
39
|
+
end
|
40
|
+
result.valid(arr)
|
41
|
+
end
|
40
42
|
end
|
41
43
|
|
42
44
|
def call(result)
|
43
|
-
return result.invalid(errors: 'is not an Array') unless result.value
|
45
|
+
return result.invalid(errors: 'is not an Array') unless ::Array === result.value
|
44
46
|
|
45
47
|
values, errors = map_array_elements(result.value)
|
46
48
|
return result.valid(values) unless errors.any?
|
@@ -50,6 +52,10 @@ module Plumb
|
|
50
52
|
|
51
53
|
private
|
52
54
|
|
55
|
+
def _inspect
|
56
|
+
%(Array[#{element_type}])
|
57
|
+
end
|
58
|
+
|
53
59
|
def map_array_elements(list)
|
54
60
|
# Reuse the same result object for each element
|
55
61
|
# to decrease object allocation.
|
@@ -73,12 +79,12 @@ module Plumb
|
|
73
79
|
errors = {}
|
74
80
|
|
75
81
|
values = list
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
+
.map { |e| Concurrent::Future.execute { element_type.resolve(e) } }
|
83
|
+
.map.with_index do |f, idx|
|
84
|
+
re = f.value
|
85
|
+
errors[idx] = f.reason if f.rejected?
|
86
|
+
re.value
|
87
|
+
end
|
82
88
|
|
83
89
|
[values, errors]
|
84
90
|
end
|
data/lib/plumb/build.rb
CHANGED
@@ -11,8 +11,11 @@ module Plumb
|
|
11
11
|
def initialize(type, factory_method: :new, &block)
|
12
12
|
@type = type
|
13
13
|
@block = block || ->(value) { type.send(factory_method, value) }
|
14
|
+
freeze
|
14
15
|
end
|
15
16
|
|
16
17
|
def call(result) = result.valid(@block.call(result.value))
|
18
|
+
|
19
|
+
private def _inspect = "Build[#{@type.inspect}]"
|
17
20
|
end
|
18
21
|
end
|
data/lib/plumb/hash_class.rb
CHANGED
@@ -12,8 +12,9 @@ module Plumb
|
|
12
12
|
|
13
13
|
attr_reader :_schema
|
14
14
|
|
15
|
-
def initialize(schema
|
15
|
+
def initialize(schema: BLANK_HASH, inclusive: false)
|
16
16
|
@_schema = wrap_keys_and_values(schema)
|
17
|
+
@inclusive = inclusive
|
17
18
|
freeze
|
18
19
|
end
|
19
20
|
|
@@ -28,12 +29,14 @@ module Plumb
|
|
28
29
|
def schema(*args)
|
29
30
|
case args
|
30
31
|
in [::Hash => hash]
|
31
|
-
|
32
|
-
in [Steppable => key_type,
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
32
|
+
self.class.new(schema: _schema.merge(wrap_keys_and_values(hash)), inclusive: @inclusive)
|
33
|
+
in [Steppable => key_type, value_type]
|
34
|
+
HashMap.new(key_type, Steppable.wrap(value_type))
|
35
|
+
in [Class => key_type, value_type]
|
36
|
+
HashMap.new(Steppable.wrap(key_type), Steppable.wrap(value_type))
|
37
|
+
else
|
38
|
+
raise ::ArgumentError, "unexpected value to Types::Hash#schema #{args.inspect}"
|
39
|
+
end
|
37
40
|
end
|
38
41
|
|
39
42
|
alias [] schema
|
@@ -45,7 +48,7 @@ module Plumb
|
|
45
48
|
def +(other)
|
46
49
|
raise ArgumentError, "expected a HashClass, got #{other.class}" unless other.is_a?(HashClass)
|
47
50
|
|
48
|
-
self.class.new(merge_rightmost_keys(_schema, other._schema))
|
51
|
+
self.class.new(schema: merge_rightmost_keys(_schema, other._schema), inclusive: @inclusive)
|
49
52
|
end
|
50
53
|
|
51
54
|
def &(other)
|
@@ -56,21 +59,43 @@ module Plumb
|
|
56
59
|
memo[k] = other.at_key(k)
|
57
60
|
end
|
58
61
|
|
59
|
-
self.class.new(intersected)
|
62
|
+
self.class.new(schema: intersected, inclusive: @inclusive)
|
60
63
|
end
|
61
64
|
|
62
65
|
def tagged_by(key, *types)
|
63
66
|
TaggedHash.new(self, key, types)
|
64
67
|
end
|
65
68
|
|
69
|
+
def inclusive
|
70
|
+
self.class.new(schema: _schema, inclusive: true)
|
71
|
+
end
|
72
|
+
|
66
73
|
def at_key(a_key)
|
67
74
|
_schema[Key.wrap(a_key)]
|
68
75
|
end
|
69
76
|
|
70
77
|
def to_h = _schema
|
71
78
|
|
72
|
-
|
73
|
-
|
79
|
+
def filtered
|
80
|
+
op = lambda do |result|
|
81
|
+
return result.invalid(errors: 'must be a Hash') unless result.value.is_a?(::Hash)
|
82
|
+
return result unless _schema.any?
|
83
|
+
|
84
|
+
input = result.value
|
85
|
+
field_result = BLANK_RESULT.dup
|
86
|
+
output = _schema.each.with_object({}) do |(key, field), ret|
|
87
|
+
key_s = key.to_sym
|
88
|
+
if input.key?(key_s)
|
89
|
+
r = field.call(field_result.reset(input[key_s]))
|
90
|
+
ret[key_s] = r.value if r.valid?
|
91
|
+
elsif !key.optional?
|
92
|
+
r = field.call(BLANK_RESULT)
|
93
|
+
ret[key_s] = r.value if r.valid?
|
94
|
+
end
|
95
|
+
end
|
96
|
+
result.valid(output)
|
97
|
+
end
|
98
|
+
Step.new(op, [_inspect, 'filtered'].join('.'))
|
74
99
|
end
|
75
100
|
|
76
101
|
def call(result)
|
@@ -80,7 +105,9 @@ module Plumb
|
|
80
105
|
input = result.value
|
81
106
|
errors = {}
|
82
107
|
field_result = BLANK_RESULT.dup
|
83
|
-
|
108
|
+
initial = {}
|
109
|
+
initial = initial.merge(input) if @inclusive
|
110
|
+
output = _schema.each.with_object(initial) do |(key, field), ret|
|
84
111
|
key_s = key.to_sym
|
85
112
|
if input.key?(key_s)
|
86
113
|
r = field.call(field_result.reset(input[key_s]))
|
@@ -98,6 +125,10 @@ module Plumb
|
|
98
125
|
|
99
126
|
private
|
100
127
|
|
128
|
+
def _inspect
|
129
|
+
%(Hash[#{_schema.map { |(k, v)| [k.inspect, v.inspect].join(': ') }.join(', ')}])
|
130
|
+
end
|
131
|
+
|
101
132
|
def wrap_keys_and_values(hash)
|
102
133
|
case hash
|
103
134
|
when ::Array
|
@@ -109,7 +140,7 @@ module Plumb
|
|
109
140
|
when Callable
|
110
141
|
hash
|
111
142
|
else # leaf values
|
112
|
-
|
143
|
+
Steppable.wrap(hash)
|
113
144
|
end
|
114
145
|
end
|
115
146
|
|