proc 0.0.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7ec097a6117d6d476f35dbd2368b5e70e74b42ef134bf2df9a6f21f81f16a1a9
4
- data.tar.gz: 5930aff0b125dc144bca46a6505b68dc049b0be416726b9f4af8fcafaa5b0a04
3
+ metadata.gz: 75ef8a4c8d8b8f07682996ca70c0263f5e0b6d123493137a07f92717c9c16eab
4
+ data.tar.gz: 48d2b0b5a4e453366ec4fc99536f98d875f804cb705ff5fde9a8b4b2d46e74c0
5
5
  SHA512:
6
- metadata.gz: 05d23e88b3c71c8fe7e06d450da67bd95054d2eecd5b25c35a5e92c2f0e0892739f5fb7d7a6c7343ef6081529ff5d1fb46f7e0f9368fc421c03187c6e16f3842
7
- data.tar.gz: b50b6af0c4cc0a383aaa6043d2e9c99689951b87934bb5746fd59084c504d597555d61619e3cade3ed2d1be51d9d064536a0e1e50d4bcb499816db4e4e072d69
6
+ metadata.gz: 1fca54bd564be4fa0ac560ee4c81c9f6d81421b6fe26a6a7038ac97c4b2a17da34359949ba9d1d02191370022d916727793bda08e3d5a3c6d50d577acfaea17b
7
+ data.tar.gz: 7a7bba01f304f8866ff2cd69b779c4a4995b966d0ec835d78da29f577704c89d848d79b83b516c5b53df59c0352fb7a038f4f28e2a2d02fab5337048cbfb6fff
@@ -0,0 +1,80 @@
1
+ The `proc` gem lets you call codeless web functions through the proc.dev service.
2
+
3
+ ## Getting Started
4
+
5
+ Install `proc` using the `gem` command from the command line:
6
+
7
+ ```
8
+ gem install proc
9
+ ```
10
+
11
+ You will also need a proc.dev account. Create a free account in seconds at [proc.dev](https://proc.dev/).
12
+
13
+ ## Connecting
14
+
15
+ Sign in to your proc.dev account and locate your secret key. Use the secret to create a client connection:
16
+
17
+ ```ruby
18
+ client = Proc.connect("secret-key")
19
+ ```
20
+
21
+ ## Calling Procs
22
+
23
+ You can call available procs through a connected client:
24
+
25
+ ```ruby
26
+ client.core.string.reverse.call("hello")
27
+ => "olleh"
28
+ ```
29
+
30
+ Requests are sent to proc.dev anytime the `call` method is invoked.
31
+
32
+ ## Callable Contexts
33
+
34
+ Proc lookups create contexts that can be called later:
35
+
36
+ ```ruby
37
+ string = client.core.string
38
+
39
+ string.reverse.call("hello")
40
+ => "olleh"
41
+
42
+ string.truncate.call("hello", length: 3)
43
+ => "hel"
44
+ ```
45
+
46
+ Contexts can be configured with default input and arguments using the `with` method:
47
+
48
+ ```ruby
49
+ truncate = client.core.string.truncate.with("default", length: 1)
50
+
51
+ truncate.call
52
+ => "d"
53
+ ```
54
+
55
+ Default input and/or arguments can be overidden for a specific call:
56
+
57
+ ```ruby
58
+ truncate.call(length: 3)
59
+ => "def"
60
+ ```
61
+
62
+ Procs can also be looked up using the hash key syntax:
63
+
64
+ ```ruby
65
+ client["core.string.truncate"].call("hello", length: 3)
66
+ => "hel"
67
+ ```
68
+
69
+ ## Compositions
70
+
71
+ Procs can be composed together to build more complex behavior:
72
+
73
+ ```ruby
74
+ composition = client.core.string.reverse >> client.core.string.truncate(length: 3) >> client.core.string.capitalize
75
+
76
+ composition.call("hello")
77
+ => "Oll"
78
+ ```
79
+
80
+ Compositions are sent to proc.dev in a single request.
@@ -6,4 +6,12 @@ class Proc
6
6
  def self.connect(authorization, **options)
7
7
  Client.new(authorization, **options)
8
8
  end
9
+
10
+ def self.undefined
11
+ @_undefined ||= Object.new
12
+ end
13
+
14
+ def self.undefined?(value)
15
+ value == undefined
16
+ end
9
17
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Proc
4
+ class Argument
5
+ def initialize(name, **options)
6
+ @name = name
7
+ @options = options
8
+ end
9
+
10
+ def serialize
11
+ {"::" => {"name" => @name.to_s}.merge(serialized_options)}
12
+ end
13
+
14
+ def serialized_options
15
+ @options.each_pair.each_with_object({}) { |(key, value), hash|
16
+ hash[key.to_s] = serialize_value(value)
17
+ }
18
+ end
19
+
20
+ private def serialize_value(value)
21
+ if value.respond_to?(:serialize)
22
+ value.serialize
23
+ else
24
+ value
25
+ end
26
+ end
27
+ end
28
+ end
@@ -2,22 +2,105 @@
2
2
 
3
3
  class Proc
4
4
  class Callable
5
- attr_reader :proc
5
+ attr_reader :proc, :input, :arguments
6
6
 
7
- def initialize(proc, client:)
8
- @proc = proc
7
+ def initialize(proc, client:, input: Proc.undefined, arguments: {})
8
+ @proc = proc.to_s
9
9
  @client = client
10
+ @input = input
11
+ @arguments = arguments
10
12
  end
11
13
 
12
- def call(input = nil, **arguments)
13
- @client.perform(@proc, input, **arguments)
14
+ def initialize_copy(_)
15
+ @input = input.dup
16
+ @arguments = arguments.dup
17
+ end
18
+
19
+ def call(input = input_omitted = true, **arguments)
20
+ callable = self.class.new(
21
+ @proc,
22
+ client: @client,
23
+ input: input_omitted ? @input : input,
24
+ arguments: @arguments.merge(arguments)
25
+ )
26
+
27
+ @client.call(@proc, callable.serialized_input, **callable.serialized_arguments)
28
+ end
29
+
30
+ def with(input = input_omitted = true, **arguments)
31
+ self.class.new(
32
+ @proc,
33
+ client: @client,
34
+ input: input_omitted ? @input : input,
35
+ arguments: @arguments.merge(arguments)
36
+ )
14
37
  end
15
38
 
16
39
  def >>(other)
17
- composed = Composition.new(client: @client)
40
+ composed = Composition.new(client: @client, input: @input)
18
41
  composed << self
19
42
  composed << other
20
43
  composed
21
44
  end
45
+
46
+ def serialize
47
+ wrapped = {"[]" => [[@proc, serialized_arguments]]}
48
+
49
+ unless Proc.undefined?(@input)
50
+ wrapped["<<"] = serialized_input
51
+ end
52
+
53
+ {"{}" => wrapped}
54
+ end
55
+
56
+ def serialized_input
57
+ serialize_value(@input)
58
+ end
59
+
60
+ def serialized_arguments
61
+ @arguments.each_pair.each_with_object({}) { |(key, value), hash|
62
+ hash[key.to_s] = serialize_value(value)
63
+ }
64
+ end
65
+
66
+ def [](proc)
67
+ Callable.new(
68
+ [@proc, proc].join("."),
69
+ client: @client,
70
+ input: @input,
71
+ arguments: @arguments
72
+ )
73
+ end
74
+
75
+ IGNORE_MISSING = %i[to_hash].freeze
76
+
77
+ def method_missing(name, input = input_omitted = true, **arguments)
78
+ if IGNORE_MISSING.include?(name)
79
+ super
80
+ else
81
+ Callable.new(
82
+ [@proc, name].join("."),
83
+ client: @client,
84
+ input: input_omitted ? @input : input,
85
+ arguments: @arguments.merge(arguments)
86
+ )
87
+ end
88
+ end
89
+
90
+ def respond_to_missing?(name, *)
91
+ if IGNORE_MISSING.include?(name)
92
+ super
93
+ else
94
+ true
95
+ end
96
+ end
97
+
98
+ private def serialize_value(value)
99
+ if value.respond_to?(:serialize)
100
+ value.serialize
101
+ else
102
+ value
103
+ end
104
+ end
22
105
  end
23
106
  end
@@ -3,6 +3,7 @@
3
3
  require "async"
4
4
  require "oj"
5
5
 
6
+ require_relative "argument"
6
7
  require_relative "callable"
7
8
  require_relative "composition"
8
9
  require_relative "null_logger"
@@ -13,7 +14,7 @@ class Proc
13
14
  class Error < StandardError
14
15
  end
15
16
 
16
- class ArgumentError < ::ArgumentError
17
+ class Invalid < ::ArgumentError
17
18
  end
18
19
 
19
20
  class Undefined < ::NameError
@@ -22,10 +23,16 @@ class Proc
22
23
  class Unauthorized < Error
23
24
  end
24
25
 
26
+ class Forbidden < Error
27
+ end
28
+
25
29
  class Unavailable < Error
26
30
  end
27
31
 
28
- class RateLimited < Error
32
+ class Limited < Error
33
+ end
34
+
35
+ class Timeout < Error
29
36
  end
30
37
 
31
38
  class Client < Http::Client
@@ -57,58 +64,94 @@ class Proc
57
64
  @resets_at
58
65
  end
59
66
 
60
- def compose(&block)
61
- Composition.new(client: self, &block)
62
- end
63
-
64
67
  DEFAULT_HEADERS = {
68
+ "accept" => "application/json",
65
69
  "content-type" => "application/json"
66
70
  }.freeze
67
71
 
68
- def perform(proc, input, **arguments)
72
+ def call(proc = nil, input = nil, **arguments)
69
73
  Async(logger: NullLogger) { |task|
70
- body = {
71
- input: input, arguments: arguments
72
- }
74
+ body = {}
75
+
76
+ unless Proc.undefined?(input)
77
+ body["<<"] = serialize_value(input)
78
+ end
79
+
80
+ arguments.each_pair do |key, value|
81
+ body[key.to_s] = serialize_value(value)
82
+ end
73
83
 
74
84
  headers = {
75
- "authorization" => "Bearer #{@authorization}"
85
+ "authorization" => "bearer #{@authorization}"
76
86
  }.merge(DEFAULT_HEADERS)
77
87
 
78
88
  begin
79
- response = call(:post, build_uri(proc), headers: headers, body: Oj.dump(body, mode: :json), task: task)
89
+ response = super(:post, build_uri(proc), headers: headers, body: Oj.dump(body, mode: :custom), task: task)
80
90
 
81
91
  @remaining = response.headers["x-rate-limit-remaining"].to_s.to_i
82
92
  @resets_at = Time.at(response.headers["x-rate-limit-reset"].to_s.to_i)
83
93
 
84
- payload = Oj.load(response.read, mode: :strict)
85
- rescue => error
86
- raise Proc::Unavailable, error.message
94
+ payload = Oj.load(response.read, mode: :compat)
95
+ rescue
96
+ raise Proc::Unavailable
87
97
  ensure
88
98
  response&.close
89
99
  end
90
100
 
91
101
  case response.status
92
102
  when 200
93
- payload["value"]
103
+ payload[">>"]
94
104
  when 400
95
- raise Proc::ArgumentError, payload.dig("error", "message")
96
- when 403
105
+ raise Proc::Invalid, payload.dig("error", "message")
106
+ when 401
97
107
  raise Proc::Unauthorized, payload.dig("error", "message")
108
+ when 403
109
+ raise Proc::Forbidden, payload.dig("error", "message")
98
110
  when 404
99
111
  raise Proc::Undefined, payload.dig("error", "message")
112
+ when 408
113
+ raise Proc::Timeout, payload.dig("error", "message")
100
114
  when 429
101
- raise Proc::RateLimited, payload.dig("error", "message")
115
+ raise Proc::Limited, payload.dig("error", "message")
102
116
  when 500
103
117
  raise Proc::Error, payload.dig("error", "message")
118
+ when 508
119
+ raise Proc::Error, payload.dig("error", "message")
120
+ else
121
+ raise Proc::Error, "unhandled"
104
122
  end
105
123
  }.wait
106
124
  end
107
125
 
126
+ def method_missing(name, input = input_omitted = true, **arguments)
127
+ if input_omitted
128
+ Callable.new(name, client: self, arguments: arguments)
129
+ else
130
+ Callable.new(name, client: self, input: input, arguments: arguments)
131
+ end
132
+ end
133
+
134
+ def respond_to_missing?(name, *)
135
+ true
136
+ end
137
+
138
+ def argument(name, **options)
139
+ Argument.new(name, **options)
140
+ end
141
+ alias_method :arg, :argument
142
+
108
143
  private def build_uri(proc)
109
- host_and_path = File.join(@host, proc.split(".").join("/"))
144
+ host_and_path = File.join(@host, proc.to_s.split(".").join("/"))
110
145
 
111
146
  "#{@scheme}://#{host_and_path}"
112
147
  end
148
+
149
+ private def serialize_value(value)
150
+ if value.respond_to?(:serialize)
151
+ value.serialize
152
+ else
153
+ value
154
+ end
155
+ end
113
156
  end
114
157
  end
@@ -2,26 +2,37 @@
2
2
 
3
3
  class Proc
4
4
  class Composition
5
- require_relative "composition/deferable"
6
- require_relative "composition/evaluator"
5
+ attr_reader :input, :callables, :arguments
7
6
 
8
- def initialize(client:, &block)
7
+ def initialize(client:, input:, callables: [], arguments: {})
9
8
  @client = client
10
- @block = block
11
- @callables = []
9
+ @input = input
10
+ @callables = callables
11
+ @arguments = arguments
12
12
  end
13
13
 
14
14
  def initialize_copy(_)
15
15
  @callables = @callables.dup
16
16
  end
17
17
 
18
- def call(input, **arguments)
19
- if @block
20
- evaluator = @block.call(Evaluator.new, **arguments)
21
- @client.perform("compose", input, procs: evaluator.serialize.concat(serialize))
22
- else
23
- @client.perform("compose", input, procs: serialize)
24
- end
18
+ def call(input = input_omitted = true, **arguments)
19
+ callable = self.class.new(
20
+ client: @client,
21
+ input: input_omitted ? @input : input,
22
+ callables: @callables.dup,
23
+ arguments: @arguments.merge(arguments)
24
+ )
25
+
26
+ @client.call("proc.exec", nil, proc: callable.serialize)
27
+ end
28
+
29
+ def with(input = input_omitted = true, **arguments)
30
+ self.class.new(
31
+ client: @client,
32
+ input: input_omitted ? @input : input,
33
+ callables: @callables.dup,
34
+ arguments: @arguments.merge(arguments)
35
+ )
25
36
  end
26
37
 
27
38
  def >>(other)
@@ -31,11 +42,59 @@ class Proc
31
42
  end
32
43
 
33
44
  def <<(callable)
34
- @callables << callable
45
+ case callable
46
+ when Composition
47
+ merge(callable)
48
+ when Callable
49
+ @callables << callable
50
+ end
51
+ end
52
+
53
+ def serialize
54
+ wrapped = {"[]" => serialized_calls}
55
+
56
+ unless Proc.undefined?(@input)
57
+ wrapped["<<"] = serialized_input
58
+ end
59
+
60
+ {"{}" => wrapped.merge(serialized_arguments)}
61
+ end
62
+
63
+ def serialized_calls
64
+ @callables.map { |callable|
65
+ arguments = callable.serialized_arguments
66
+
67
+ unless Proc.undefined?(callable.input)
68
+ arguments["<<"] = callable.serialized_input
69
+ end
70
+
71
+ [callable.proc, arguments]
72
+ }
73
+ end
74
+
75
+ def serialized_input
76
+ serialize_value(@input)
77
+ end
78
+
79
+ def serialized_arguments
80
+ @arguments.each_pair.each_with_object({}) { |(key, value), hash|
81
+ hash[key.to_s] = serialize_value(value)
82
+ }
83
+ end
84
+
85
+ def merge(composition)
86
+ raise ArgumentError, "expected a composition" unless composition.is_a?(self.class)
87
+
88
+ @callables.concat(composition.callables)
89
+ @arguments.merge!(composition.arguments)
35
90
  end
36
91
 
37
- protected def serialize
38
- @callables.map(&:proc)
92
+ private def serialize_value(value)
93
+ if value.respond_to?(:serialize)
94
+ value.serialize
95
+ else
96
+ value
97
+ end
39
98
  end
40
99
  end
41
100
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Proc
4
- VERSION = "0.0.3"
4
+ VERSION = "0.2.0"
5
5
 
6
6
  def self.version
7
7
  VERSION
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: proc
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bryan Powell
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-10-21 00:00:00.000000000 Z
11
+ date: 2020-11-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async-http
@@ -45,13 +45,12 @@ extensions: []
45
45
  extra_rdoc_files: []
46
46
  files:
47
47
  - LICENSE
48
- - lib/examples/scratch.rb
48
+ - README.md
49
49
  - lib/proc.rb
50
+ - lib/proc/argument.rb
50
51
  - lib/proc/callable.rb
51
52
  - lib/proc/client.rb
52
53
  - lib/proc/composition.rb
53
- - lib/proc/composition/deferable.rb
54
- - lib/proc/composition/evaluator.rb
55
54
  - lib/proc/http/client.rb
56
55
  - lib/proc/http/request.rb
57
56
  - lib/proc/http/response.rb
@@ -1,61 +0,0 @@
1
- # Pass `token: "..."` or default to `PROC_ACCESS_TOKEN`:
2
- #
3
- client = Proc.connect
4
-
5
- # Not chainable, returns the value:
6
- #
7
- client.stdlib.echo("bar")
8
- # => bar
9
-
10
- # Create chains using `chain` (perhaps alias as `with`):
11
- #
12
- client.chain("foo") { |chain| chain.stdlib.reverse.capitalize.truncate(2) }
13
- # => Oo
14
-
15
- # Or return a chain to pass around:
16
- #
17
- chain = client.chain("foo")
18
- # => <Proc::Chain ...>
19
- chain.stdlib.echo("bar")
20
-
21
- # Call `value` or `perform` to evaluate the chain:
22
- #
23
- chain.value
24
-
25
- # Calling a non-existent proc raises a Proc::NameError,
26
- #
27
- chain.nonexistent
28
-
29
- # Pass values through different contexts:
30
- #
31
- client.chain("foo") { |chain| chain.stdlib.reverse >> chain.whatever.other }
32
-
33
- # Use from the cli (included with the ruby gem OR as a go library):
34
- #
35
- # proc echo "bar" -t access-token
36
- # proc chain "bar" "stdlib.reverse >> whatever.other" -t access-token
37
-
38
- #########################################################
39
- # alternatives so that we don't have to fetch metadata: #
40
- #########################################################
41
-
42
- proc = Proc.connect
43
-
44
- # I like this because it's obvious how to get a reference as well as just make the call.
45
- #
46
- proc["stdlib.echo"].call("foo")
47
-
48
- # This works for simple composition, where the value is simply passed through.
49
- #
50
- my_proc = proc["stdlib.reverse"] >> proc["stdlib.capitalize"]
51
-
52
- my_proc.call("foo")
53
-
54
- # Perhaps something like this, where we compose a proc with arguments. Each is implicitly called, or
55
- # call explicitly to pass an argument to the proc. I sort of like the simplicity of this.
56
- #
57
- my_proc = proc.compose { |context, length:|
58
- context["stdlib.reverse"] >> context["stdlib.capitalize"] >> context["stdlib.truncate"].call(length)
59
- }
60
-
61
- my_proc.call("foo", length: 2)
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Proc
4
- class Composition
5
- class Deferable
6
- attr_reader :proc
7
- attr_writer :arguments
8
-
9
- def initialize(proc)
10
- @proc = proc
11
- @arguments = {}
12
- end
13
-
14
- def serialize
15
- [@proc, @arguments]
16
- end
17
- end
18
- end
19
- end
@@ -1,52 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Proc
4
- class Composition
5
- class Evaluator
6
- attr_reader :callables
7
-
8
- def initialize
9
- @callables = []
10
- end
11
-
12
- def initialize_copy(_)
13
- @callables = @callables.map(&:dup)
14
- end
15
-
16
- def [](proc)
17
- evaluator = dup
18
- evaluator << Deferable.new(proc)
19
- evaluator
20
- end
21
-
22
- def <<(proc)
23
- @callables << proc
24
- end
25
-
26
- def >>(other)
27
- evaluator = dup
28
-
29
- case other
30
- when Evaluator
31
- other.callables.each do |each_callable|
32
- evaluator << each_callable
33
- end
34
- when Callable
35
- evaluator << Deferable.new(other.proc)
36
- end
37
-
38
- evaluator
39
- end
40
-
41
- def call(**arguments)
42
- evaluator = dup
43
- evaluator.callables.last.arguments = arguments
44
- evaluator
45
- end
46
-
47
- def serialize
48
- @callables.map(&:serialize)
49
- end
50
- end
51
- end
52
- end