proc 0.0.2 → 0.1.2

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: 59becc885513c542ccee8100b436c2854d5c42939b72424a691a281741861728
4
- data.tar.gz: 82cfac2a503a9e934efeae9b41d037b6dd72d1e78e493a6ec921d04a39fec97e
3
+ metadata.gz: 2950f8e2178a95a40c29372c902203d883bda75ac84785a3579134b9394d7a52
4
+ data.tar.gz: 6c8433c3f49bbc0a6a62187c9b992fd7c39e6eac2894f7d71a2056205bdc7900
5
5
  SHA512:
6
- metadata.gz: 71cbb386c7e906584a706538f3cccba04a7090496486b80160b2856b2d4dbe480d35b02f83814643fb208173518053f283b9d4e2c365f8164781e569b23f98ea
7
- data.tar.gz: '095adc172bb26b52871a0fb9d8265fb782cab0ae43981c03f1bedfe47200cdf1551e78cf1564e29ade46a535f0c717350e306513583b4ced7e09eac815a10c73'
6
+ metadata.gz: 36828988724d7e3cad08a3a741f576bfa50f8d53a459062892bb270e01da1d8a1ddf4977255976a314a15b4e79e8266da4089c9cdbb8f723c30d681099654c7f
7
+ data.tar.gz: e224df7dcabcf284d5cf72f410be3ccf713dfbb0ff386ae5a50597e5c9a6b07634c87759f30d5e4b4a95fc0091d0c2f0aa490146a02b8806a3784033025fd123
@@ -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.
@@ -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,104 @@
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
+ 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
+ )
37
+ end
38
+
39
+ def >>(other)
40
+ composed = Composition.new(client: @client, input: @input)
18
41
  composed << self
19
- composed << callable
42
+ composed << other
20
43
  composed
21
44
  end
45
+
46
+ def serialize
47
+ {
48
+ "{}" => {
49
+ "<<" => serialized_input,
50
+ "[]" => [[@proc, serialized_arguments]]
51
+ }
52
+ }
53
+ end
54
+
55
+ def serialized_input
56
+ serialize_value(@input)
57
+ end
58
+
59
+ def serialized_arguments
60
+ @arguments.each_pair.each_with_object({}) { |(key, value), hash|
61
+ hash[key.to_s] = serialize_value(value)
62
+ }
63
+ end
64
+
65
+ def [](proc)
66
+ Callable.new(
67
+ [@proc, proc].join("."),
68
+ client: @client,
69
+ input: @input,
70
+ arguments: @arguments
71
+ )
72
+ end
73
+
74
+ IGNORE_MISSING = %i[to_hash].freeze
75
+
76
+ def method_missing(name, input = input_omitted = true, **arguments)
77
+ if IGNORE_MISSING.include?(name)
78
+ super
79
+ else
80
+ Callable.new(
81
+ [@proc, name].join("."),
82
+ client: @client,
83
+ input: input_omitted ? @input : input,
84
+ arguments: @arguments.merge(arguments)
85
+ )
86
+ end
87
+ end
88
+
89
+ def respond_to_missing?(name, *)
90
+ if IGNORE_MISSING.include?(name)
91
+ super
92
+ else
93
+ true
94
+ end
95
+ end
96
+
97
+ private def serialize_value(value)
98
+ if value.respond_to?(:serialize)
99
+ value.serialize
100
+ else
101
+ value
102
+ end
103
+ end
22
104
  end
23
105
  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,9 +23,18 @@ 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
 
32
+ class Limited < Error
33
+ end
34
+
35
+ class Timeout < Error
36
+ end
37
+
28
38
  class Client < Http::Client
29
39
  def initialize(authorization, scheme: "https", host: "proc.dev")
30
40
  @authorization = authorization
@@ -38,29 +48,49 @@ class Proc
38
48
  Callable.new(proc, client: self)
39
49
  end
40
50
 
41
- def compose(&block)
42
- Composition.new(client: self, &block)
51
+ def remaining
52
+ unless defined?(@remaining)
53
+ self["ping"].call
54
+ end
55
+
56
+ @remaining
57
+ end
58
+
59
+ def resets_at
60
+ unless defined?(@resets_at)
61
+ self["ping"].call
62
+ end
63
+
64
+ @resets_at
43
65
  end
44
66
 
45
67
  DEFAULT_HEADERS = {
68
+ "accept" => "application/json",
46
69
  "content-type" => "application/json"
47
70
  }.freeze
48
71
 
49
- def perform(proc, input, **arguments)
72
+ def call(proc = nil, input = nil, **arguments)
50
73
  Async(logger: NullLogger) { |task|
51
- body = {
52
- input: input, arguments: arguments
53
- }
74
+ body = { "<<" => serialize_value(input) }
75
+
76
+ arguments.each_pair do |key, value|
77
+ body[key.to_s] = serialize_value(value)
78
+ end
54
79
 
55
80
  headers = {
56
- "authorization" => "Bearer #{@authorization}"
81
+ "authorization" => "bearer #{@authorization}"
57
82
  }.merge(DEFAULT_HEADERS)
58
83
 
59
84
  begin
60
- response = call(:post, build_uri(proc), headers: headers, body: Oj.dump(body, mode: :json), task: task)
85
+ response = super(:post, build_uri(proc), headers: headers, body: Oj.dump(body, mode: :custom), task: task)
61
86
 
62
- payload = Oj.load(response.read, mode: :strict)
87
+ @remaining = response.headers["x-rate-limit-remaining"].to_s.to_i
88
+ @resets_at = Time.at(response.headers["x-rate-limit-reset"].to_s.to_i)
89
+
90
+ payload = Oj.load(response.read, mode: :compat)
63
91
  rescue => error
92
+ # TODO: This should wrap `error`.
93
+ #
64
94
  raise Proc::Unavailable, error.message
65
95
  ensure
66
96
  response&.close
@@ -68,23 +98,54 @@ class Proc
68
98
 
69
99
  case response.status
70
100
  when 200
71
- payload["value"]
101
+ payload[">>"]
72
102
  when 400
73
- raise Proc::ArgumentError, payload.dig("error", "message")
74
- when 403
103
+ raise Proc::Invalid, payload.dig("error", "message")
104
+ when 401
75
105
  raise Proc::Unauthorized, payload.dig("error", "message")
106
+ when 403
107
+ raise Proc::Forbidden, payload.dig("error", "message")
76
108
  when 404
77
109
  raise Proc::Undefined, payload.dig("error", "message")
110
+ when 408
111
+ raise Proc::Timeout, payload.dig("error", "message")
112
+ when 429
113
+ raise Proc::Limited, payload.dig("error", "message")
78
114
  when 500
79
115
  raise Proc::Error, payload.dig("error", "message")
116
+ when 508
117
+ raise Proc::Error, payload.dig("error", "message")
118
+ else
119
+ raise Proc::Error, "unhandled"
80
120
  end
81
121
  }.wait
82
122
  end
83
123
 
124
+ def method_missing(name, input = nil, **arguments)
125
+ Callable.new(name, client: self, input: input, arguments: arguments)
126
+ end
127
+
128
+ def respond_to_missing?(name, *)
129
+ true
130
+ end
131
+
132
+ def argument(name, **options)
133
+ Argument.new(name, **options)
134
+ end
135
+ alias_method :arg, :argument
136
+
84
137
  private def build_uri(proc)
85
- host_and_path = File.join(@host, proc.split(".").join("/"))
138
+ host_and_path = File.join(@host, proc.to_s.split(".").join("/"))
86
139
 
87
140
  "#{@scheme}://#{host_and_path}"
88
141
  end
142
+
143
+ private def serialize_value(value)
144
+ if value.respond_to?(:serialize)
145
+ value.serialize
146
+ else
147
+ value
148
+ end
149
+ end
89
150
  end
90
151
  end
@@ -2,31 +2,49 @@
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)
25
27
  end
26
28
 
27
- def >>(callable)
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
+ )
36
+ end
37
+
38
+ def >>(other)
28
39
  composed = dup
29
- composed << callable
40
+
41
+ case other
42
+ when Composition
43
+ composed.merge(other)
44
+ when Callable
45
+ composed << other
46
+ end
47
+
30
48
  composed
31
49
  end
32
50
 
@@ -34,8 +52,44 @@ class Proc
34
52
  @callables << callable
35
53
  end
36
54
 
37
- protected def serialize
38
- @callables.map(&:proc)
55
+ def serialize
56
+ {
57
+ "{}" => {
58
+ "<<" => serialized_input,
59
+ "[]" => serialized_calls
60
+ }.merge(serialized_arguments)
61
+ }
62
+ end
63
+
64
+ def serialized_calls
65
+ @callables.map { |callable|
66
+ [callable.proc, callable.serialized_arguments]
67
+ }
68
+ end
69
+
70
+ def serialized_input
71
+ serialize_value(@input)
72
+ end
73
+
74
+ def serialized_arguments
75
+ @arguments.each_pair.each_with_object({}) { |(key, value), hash|
76
+ hash[key.to_s] = serialize_value(value)
77
+ }
78
+ end
79
+
80
+ def merge(composition)
81
+ raise ArgumentError, "expected a composition" unless composition.is_a?(self.class)
82
+
83
+ @callables.concat(composition.callables)
84
+ @arguments.merge!(composition.arguments)
85
+ end
86
+
87
+ private def serialize_value(value)
88
+ if value.respond_to?(:serialize)
89
+ value.serialize
90
+ else
91
+ value
92
+ end
39
93
  end
40
94
  end
41
95
  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,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Proc
4
- VERSION = "0.0.2"
4
+ VERSION = "0.1.2"
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.2
4
+ version: 0.1.2
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-10-31 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 >>(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