proc 0.0.1 → 0.1.1

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: 9b1cac83c7cff9f09dc94086b6b58e43cf686c4faa28e95aecda09c73e9eebbc
4
- data.tar.gz: e349377c73af33cdff5dfb488dbfd199ec37b9c1db84132e82dde7d65889b71b
3
+ metadata.gz: 63fefe7d8d6f97ef667ca24f07ef5f38b402c6982d783582d6ea869dbfc67c76
4
+ data.tar.gz: ceaeee3d8d634ba9d9ba6771266554e8dc7b8662b819751520658b0e657dbfa8
5
5
  SHA512:
6
- metadata.gz: e9b7322deb5945ddac4cff0c12c444d8c90d88b917619176bc2983370e215f54ebafdf83e82a4778424fc94a9ed67149596ebb7fd7378d0bea8d0fcdac611a67
7
- data.tar.gz: 91286717d2ee7d67a8021f3aedf2debec4c2da88e2f9abbe34dadd751f02c64f227a40dbdb8e3f6b95ed0ca5ead33557b096683594b3b5e32f10aab5eaf23367
6
+ metadata.gz: 1c44c09c6526286bbcc8e67fefd1c2d13cb5dec65b4471452eb07c9845f36e8c2aa92dcd7c5080ff7070a2a1228b6f3667c7d9616f35e37ba7d14ec9f882ac22
7
+ data.tar.gz: a38fe68ec69ed2b086741d46728245d3351fe58ed26c956fcb9ec67257e9089b9e2e383a8551530bed293188f6e14ca9799ebaf57859163bf1b9fbd0bfb26f82
@@ -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.
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # TODO: Introduce inspectable everywhere (just bundle from pakyow).
4
-
5
3
  require_relative "proc/client"
6
4
 
7
5
  class Proc
@@ -2,22 +2,79 @@
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:)
7
+ def initialize(proc, client:, input: nil, arguments: {})
8
8
  @proc = proc
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
14
17
  end
15
18
 
16
- def >>(callable)
17
- composed = Composition.new(client: @client)
19
+ def call(input = input_omitted = true, **arguments)
20
+ @client.call(@proc, input_omitted ? @input : input, **@arguments.merge(arguments))
21
+ end
22
+
23
+ def with(input = input_omitted = true, **arguments)
24
+ self.class.new(@proc, client: @client, input: input_omitted ? @input : input, arguments: @arguments.merge(arguments))
25
+ end
26
+
27
+ def >>(other)
28
+ composed = Composition.new(client: @client, input: @input)
18
29
  composed << self
19
- composed << callable
30
+ composed << other
20
31
  composed
21
32
  end
33
+
34
+ def serialize
35
+ {
36
+ "{}" => {
37
+ "<<" => serialize_value(@input),
38
+ "[]" => [[@proc, serialized_arguments]]
39
+ }
40
+ }
41
+ end
42
+
43
+ def serialized_arguments
44
+ @arguments.each_pair.each_with_object({}) { |(key, value), hash|
45
+ hash[key.to_s] = serialize_value(value)
46
+ }
47
+ end
48
+
49
+ IGNORE_MISSING = %i[to_hash].freeze
50
+
51
+ def method_missing(name, input = input_omitted = true, **arguments)
52
+ if IGNORE_MISSING.include?(name)
53
+ super
54
+ else
55
+ Callable.new(
56
+ [@proc, name].join("."),
57
+ client: @client,
58
+ input: input_omitted ? @input : input,
59
+ arguments: @arguments.merge(arguments)
60
+ )
61
+ end
62
+ end
63
+
64
+ def respond_to_missing?(name, *)
65
+ if IGNORE_MISSING.include?(name)
66
+ super
67
+ else
68
+ true
69
+ end
70
+ end
71
+
72
+ private def serialize_value(value)
73
+ if value.respond_to?(:serialize)
74
+ value.serialize
75
+ else
76
+ value
77
+ end
78
+ end
22
79
  end
23
80
  end
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "async"
3
4
  require "oj"
4
5
 
5
6
  require_relative "callable"
6
7
  require_relative "composition"
8
+ require_relative "null_logger"
7
9
 
8
10
  require_relative "http/client"
9
11
 
@@ -23,6 +25,12 @@ class Proc
23
25
  class Unavailable < Error
24
26
  end
25
27
 
28
+ class RateLimited < Error
29
+ end
30
+
31
+ class Timeout < Error
32
+ end
33
+
26
34
  class Client < Http::Client
27
35
  def initialize(authorization, scheme: "https", host: "proc.dev")
28
36
  @authorization = authorization
@@ -36,51 +44,94 @@ class Proc
36
44
  Callable.new(proc, client: self)
37
45
  end
38
46
 
39
- def compose(&block)
40
- Composition.new(client: self, &block)
47
+ def remaining
48
+ unless defined?(@remaining)
49
+ self["ping"].call
50
+ end
51
+
52
+ @remaining
41
53
  end
42
54
 
55
+ def resets_at
56
+ unless defined?(@resets_at)
57
+ self["ping"].call
58
+ end
59
+
60
+ @resets_at
61
+ end
62
+
63
+
43
64
  DEFAULT_HEADERS = {
65
+ "accept" => "application/json",
44
66
  "content-type" => "application/json"
45
67
  }.freeze
46
68
 
47
- def perform(proc, input, **arguments)
48
- body = {
49
- input: input, arguments: arguments
50
- }
51
-
52
- headers = {
53
- "authorization" => "Bearer #{@authorization}"
54
- }.merge(DEFAULT_HEADERS)
55
-
56
- begin
57
- response = call(:post, build_uri(proc), headers: headers, body: Oj.dump(body, mode: :json))
58
- rescue => error
59
- raise Proc::Unavailable, error.message
60
- end
69
+ def call(proc = nil, input = nil, **arguments)
70
+ Async(logger: NullLogger) { |task|
71
+ body = { "<<" => serialize_value(input) }
72
+
73
+ arguments.each_pair do |key, value|
74
+ body[key.to_s] = serialize_value(value)
75
+ end
76
+
77
+ headers = {
78
+ "authorization" => "bearer #{@authorization}"
79
+ }.merge(DEFAULT_HEADERS)
80
+
81
+ begin
82
+ response = super(:post, build_uri(proc), headers: headers, body: Oj.dump(body, mode: :custom), task: task)
83
+
84
+ @remaining = response.headers["x-rate-limit-remaining"].to_s.to_i
85
+ @resets_at = Time.at(response.headers["x-rate-limit-reset"].to_s.to_i)
86
+
87
+ payload = Oj.load(response.read, mode: :compat)
88
+ rescue => error
89
+ raise Proc::Unavailable, error.message
90
+ ensure
91
+ response&.close
92
+ end
93
+
94
+ case response.status
95
+ when 200
96
+ payload[">>"]
97
+ when 400
98
+ raise Proc::ArgumentError, payload.dig("error", "message")
99
+ when 403
100
+ raise Proc::Unauthorized, payload.dig("error", "message")
101
+ when 404
102
+ raise Proc::Undefined, payload.dig("error", "message")
103
+ when 408
104
+ raise Proc::Timeout, payload.dig("error", "message")
105
+ when 429
106
+ raise Proc::RateLimited, payload.dig("error", "message")
107
+ when 500
108
+ raise Proc::Error, payload.dig("error", "message")
109
+ else
110
+ raise Proc::Error, "unhandled"
111
+ end
112
+ }.wait
113
+ end
61
114
 
62
- payload = if response.body
63
- Oj.load(response.body.read, mode: :strict)
64
- end
115
+ def method_missing(name, input = nil, **arguments)
116
+ Callable.new(name, client: self, input: input, arguments: arguments)
117
+ end
65
118
 
66
- case response.status
67
- when 200
68
- payload["value"]
69
- when 400
70
- raise Proc::ArgumentError, payload.dig("error", "message")
71
- when 403
72
- raise Proc::Unauthorized, payload.dig("error", "message")
73
- when 404
74
- raise Proc::Undefined, payload.dig("error", "message")
75
- when 500
76
- raise Proc::Error, payload.dig("error", "message")
77
- end
119
+ def respond_to_missing?(name, *)
120
+ true
78
121
  end
79
122
 
80
123
  private def build_uri(proc)
81
- host_and_path = File.join(@host, proc.split(".").join("/"))
124
+ host_and_path = File.join(@host, proc.to_s.split(".").join("/"))
82
125
 
83
126
  "#{@scheme}://#{host_and_path}"
84
127
  end
128
+
129
+ private def serialize_value(value)
130
+ if value.respond_to?(:serialize)
131
+ value.serialize
132
+ else
133
+ value
134
+ end
135
+ end
85
136
  end
86
137
  end
@@ -2,31 +2,27 @@
2
2
 
3
3
  class Proc
4
4
  class Composition
5
- require_relative "composition/deferable"
6
- require_relative "composition/evaluator"
7
-
8
- def initialize(client:, &block)
5
+ def initialize(client:, input:, callables: [])
9
6
  @client = client
10
- @block = block
11
- @callables = []
7
+ @input = input
8
+ @callables = callables
12
9
  end
13
10
 
14
11
  def initialize_copy(_)
15
12
  @callables = @callables.dup
16
13
  end
17
14
 
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
15
+ def call(input = input_omitted = true, **arguments)
16
+ @client.call("exec", input_omitted ? @input : input, proc: serialized_calls)
17
+ end
18
+
19
+ def with(input = input_omitted = true)
20
+ self.class.new(client: @client, input: input_omitted ? @input : input, callables: @callables.dup)
25
21
  end
26
22
 
27
- def >>(callable)
23
+ def >>(other)
28
24
  composed = dup
29
- composed << callable
25
+ composed << other
30
26
  composed
31
27
  end
32
28
 
@@ -34,8 +30,19 @@ class Proc
34
30
  @callables << callable
35
31
  end
36
32
 
37
- protected def serialize
38
- @callables.map(&:proc)
33
+ def serialize
34
+ {
35
+ "{}" => {
36
+ "<<" => @input,
37
+ "[]" => serialized_calls
38
+ }
39
+ }
40
+ end
41
+
42
+ def serialized_calls
43
+ @callables.map { |callable|
44
+ [callable.proc, callable.serialized_arguments]
45
+ }
39
46
  end
40
47
  end
41
48
  end
@@ -8,35 +8,31 @@ require "protocol/http/body/streamable"
8
8
  require_relative "request"
9
9
  require_relative "response"
10
10
 
11
+ require_relative "../null_logger"
12
+
11
13
  class Proc
12
14
  module Http
13
15
  class Client
14
- class NullLogger
15
- class << self
16
- def method_missing(*, **)
17
- end
18
-
19
- def respond_to_missing?(*)
20
- true
21
- end
22
- end
23
- end
24
-
25
16
  def initialize
26
17
  @internet = Async::HTTP::Internet.new
27
18
  @responses = {}
28
19
  end
29
20
 
30
- def call(method, uri, params: {}, headers: {}, body: nil)
21
+ def call(method, uri, params: {}, headers: {}, body: nil, task: nil)
31
22
  request = Request.new(method: method, uri: uri, params: params, headers: headers, body: body)
32
23
 
33
- Async(logger: NullLogger) {
34
- async_request = @internet.call(*request.callable)
35
- wrap_async_response_for_request(async_request, request)
36
- }.wait
24
+ if task
25
+ make_request(request)
26
+ else
27
+ Async(logger: NullLogger) {
28
+ make_request(request)
29
+ }.wait
30
+ end
37
31
  end
38
32
 
39
33
  def close
34
+ # TODO: Make sure this works. We should also close / clear after accumulating some amount.
35
+ #
40
36
  @responses.each_value(&:close)
41
37
  @responses.clear
42
38
  @internet.close
@@ -46,20 +42,13 @@ class Proc
46
42
  @responses.count
47
43
  end
48
44
 
49
- private def wrap_async_response_for_request(async_response, request)
50
- Protocol::HTTP::Body::Streamable.wrap(async_response) do
51
- @responses.delete(async_response)
52
- end
53
-
54
- response = Response.new(
55
- request: request,
56
- status: async_response.status,
57
- version: async_response.version,
58
- headers: async_response.headers,
59
- body: async_response.body
60
- )
45
+ private def make_request(request)
46
+ async_response = @internet.call(*request.callable)
47
+ wrap_async_response_for_request(async_response, request)
48
+ end
61
49
 
62
- @responses[async_response] = response
50
+ private def wrap_async_response_for_request(async_response, request)
51
+ @responses[async_response] = Response.new(request, async_response)
63
52
  end
64
53
  end
65
54
  end
@@ -14,7 +14,7 @@ class Proc
14
14
  end
15
15
 
16
16
  def callable
17
- return @method.to_s.upcase, finalize_uri(@uri, @params), @headers, @body
17
+ [@method.to_s.upcase, finalize_uri(@uri, @params), @headers, @body]
18
18
  end
19
19
 
20
20
  private def finalize_uri(uri, params)
@@ -1,37 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "forwardable"
4
+
3
5
  class Proc
4
6
  module Http
5
7
  class Response
6
- attr_reader :request, :status, :version, :headers, :body
7
-
8
- def initialize(request:, status:, version:, headers:, body:)
9
- @request = request
10
- @status = status
11
- @version = version
12
- @headers = headers
13
- @body = body
14
-
15
- @stream = nil
16
- @stream_blocks = []
17
- end
18
-
19
- def stream(&block)
20
- @stream_blocks << block
8
+ extend Forwardable
9
+ def_delegators :@response, :status, :version, :headers, :read, :close
21
10
 
22
- @stream ||= Async {
23
- @body.each do |chunk|
24
- @stream_blocks.each do |stream_callback|
25
- stream_callback.call(chunk)
26
- end
27
- end
28
- }
29
- end
11
+ attr_reader :request
30
12
 
31
- def close
32
- @stream&.stop
33
- @stream_blocks.clear
34
- @body.close
13
+ def initialize(request, response)
14
+ @request = request
15
+ @response = response
35
16
  end
36
17
  end
37
18
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Proc
4
+ class NullLogger
5
+ class << self
6
+ def method_missing(*, **)
7
+ end
8
+
9
+ def respond_to_missing?(*)
10
+ true
11
+ end
12
+ end
13
+ end
14
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Proc
4
- VERSION = "0.0.1"
4
+ VERSION = "0.1.1"
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.1
4
+ version: 0.1.1
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-20 00:00:00.000000000 Z
11
+ date: 2020-10-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async-http
@@ -45,16 +45,15 @@ 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
50
  - lib/proc/callable.rb
51
51
  - lib/proc/client.rb
52
52
  - lib/proc/composition.rb
53
- - lib/proc/composition/deferable.rb
54
- - lib/proc/composition/evaluator.rb
55
53
  - lib/proc/http/client.rb
56
54
  - lib/proc/http/request.rb
57
55
  - lib/proc/http/response.rb
56
+ - lib/proc/null_logger.rb
58
57
  - lib/proc/version.rb
59
58
  homepage: https://proc.dev/
60
59
  licenses:
@@ -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 >>(callable)
27
- evaluator = dup
28
-
29
- case callable
30
- when Evaluator
31
- callable.callables.each do |each_callable|
32
- evaluator << each_callable
33
- end
34
- when Callable
35
- evaluator << Deferable.new(callable.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