proc 0.0.1 → 0.1.1

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: 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