proc 0.0.3 → 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 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