thron 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.env.example +4 -0
  3. data/.gitignore +13 -0
  4. data/.travis.yml +6 -0
  5. data/Gemfile +4 -0
  6. data/README.md +182 -0
  7. data/Rakefile +10 -0
  8. data/Vagrantfile +80 -0
  9. data/bin/console +14 -0
  10. data/bin/setup +7 -0
  11. data/config/thron.yml +10 -0
  12. data/lib/thron.rb +3 -0
  13. data/lib/thron/circuit_breaker.rb +46 -0
  14. data/lib/thron/config.rb +35 -0
  15. data/lib/thron/entity/base.rb +78 -0
  16. data/lib/thron/entity/image.rb +30 -0
  17. data/lib/thron/gateway/access_manager.rb +69 -0
  18. data/lib/thron/gateway/apps.rb +91 -0
  19. data/lib/thron/gateway/apps_admin.rb +153 -0
  20. data/lib/thron/gateway/base.rb +41 -0
  21. data/lib/thron/gateway/category.rb +210 -0
  22. data/lib/thron/gateway/client.rb +59 -0
  23. data/lib/thron/gateway/comment.rb +76 -0
  24. data/lib/thron/gateway/contact.rb +120 -0
  25. data/lib/thron/gateway/content.rb +267 -0
  26. data/lib/thron/gateway/content_category.rb +32 -0
  27. data/lib/thron/gateway/content_list.rb +40 -0
  28. data/lib/thron/gateway/dashboard.rb +120 -0
  29. data/lib/thron/gateway/delivery.rb +122 -0
  30. data/lib/thron/gateway/device.rb +58 -0
  31. data/lib/thron/gateway/metadata.rb +68 -0
  32. data/lib/thron/gateway/publish_in_weebo_express.rb +39 -0
  33. data/lib/thron/gateway/publishing_process.rb +141 -0
  34. data/lib/thron/gateway/repository.rb +111 -0
  35. data/lib/thron/gateway/session.rb +15 -0
  36. data/lib/thron/gateway/users_group_manager.rb +117 -0
  37. data/lib/thron/gateway/v_user_manager.rb +195 -0
  38. data/lib/thron/logger.rb +25 -0
  39. data/lib/thron/pageable.rb +26 -0
  40. data/lib/thron/paginator.rb +82 -0
  41. data/lib/thron/response.rb +48 -0
  42. data/lib/thron/root.rb +9 -0
  43. data/lib/thron/routable.rb +80 -0
  44. data/lib/thron/route.rb +102 -0
  45. data/lib/thron/string_extensions.rb +23 -0
  46. data/lib/thron/user.rb +77 -0
  47. data/lib/thron/version.rb +3 -0
  48. data/log/.gitignore +4 -0
  49. data/thron.gemspec +26 -0
  50. metadata +176 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 828717f3d643a94ede20ded4f7d5ffec9515244c
4
+ data.tar.gz: 6ac41767ca932862c830b22933651e49b8d68e45
5
+ SHA512:
6
+ metadata.gz: a37a51677d948af316337b411a051add6b6d78717fb20d2531076912040559349f4a2ec7023523d12e93ce41eb48fd29c65f0f60cc421e86ea880045ab1bfe30
7
+ data.tar.gz: 6f21a2b5e729e4cef36063593218acad88c46c838c4ebc37a6bfa687a04823a7d7223d6b4b8f12cbfcd13b841e2e3e025477722824109c25e0bd9df90eade41c
@@ -0,0 +1,4 @@
1
+ THRON_CLIENT_ID=<the client id subscribed to Thron APIs>
2
+ VERBOSE=<a boolean indicating if the remote calls has to be printed on screen, useful when dealing with REPL>
3
+ LOGGER_LEVEL=<the logger level you want, default to WARN>
4
+ CIRCUIT_BREAKER_THRESHOLD=<the number of tries before circuit breaker open the circuit in case of communication issues, set to zero to disable the circuit breaker>
@@ -0,0 +1,13 @@
1
+ .env
2
+ /tags
3
+ /.vagrant/
4
+ /.bundle/
5
+ /.yardoc
6
+ /Gemfile.lock
7
+ /_yardoc/
8
+ /coverage/
9
+ /doc/
10
+ /pkg/
11
+ /spec/reports/
12
+ /tmp/
13
+ *.gem
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.8
4
+ - 2.2.1
5
+ - 2.3.0
6
+ before_install: gem install bundler -v 1.11.2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in thron.gemspec
4
+ gemspec
@@ -0,0 +1,182 @@
1
+ ## Table of Contents
2
+ * [Warning](#warning)
3
+ * [Scope](#scope)
4
+ * [Setup](#setup)
5
+ * [HTTPArty](#httparty)
6
+ * [Architecture](#architecture)
7
+ * [Gateways](#gateways)
8
+ * [Routing](#routing)
9
+ * [Paginator](#paginator)
10
+ * [Response](#response)
11
+ * [Dynamic Entities](#dynamic-entities)
12
+ * [User Aggregate](#user-aggregate)
13
+ * [Interface](#interface)
14
+ * [Access](#access)
15
+ * [Contents](#contents)
16
+ * [Groups](#groups)
17
+ * [Categories](#categories)
18
+ * [Disguise](#disguise)
19
+
20
+ ## Warning
21
+ I do not maintain this gem anymore. Please do copy with latest Thron APIs and integrate them into this facade if you need to use it.
22
+
23
+ ## Scope
24
+ This gem provides a simple Ruby client for the [Thron](https://developer.4me.it/index.php) (ex 4me) APIs.
25
+ The aim of this gem is to provide a simple interface for your ruby application
26
+ that need to communicate with Thron services.
27
+ I've also managed to keep the gem's dependencies footprint as small as possible (i
28
+ hate bulky Gemfile...).
29
+
30
+ ## Setup
31
+ This gem use at least a **Ruby 2.1** compatible parser.
32
+ Thron is a payment service, is assumed you have a valid account in order to use this gem.
33
+ Once you have valid credentials to access the Thron APIs, you have to enter the **THRON_CLIENT_ID** value
34
+ inside of your *.env* file, this way the gem can reads from it and configure itself properly.
35
+
36
+ ## HTTParty
37
+ This gem uses the [HTTParty](https://github.com/jnunemaker/httparty) library to communicate with the Thron APIs.
38
+ HTTParty has proven to be reliable and simple, so it's a natural candidate.
39
+
40
+ ## Architecture
41
+ This gem is architected on these concepts:
42
+
43
+ ### Gateways
44
+ Several gateways objects are used to communicate with the Thron APIs: each of them
45
+ mimic the original Thron API namespace (find a complete list on Thron site).
46
+ Each gateway derive from a basic class, which includes the HTTParty interface.
47
+
48
+ #### Routing
49
+ A simple routing engine is encapsulated into the gateway objects: a class-level
50
+ hash declares the routes that matches the API name, by specifying the
51
+ format and verb.
52
+ Some extra parameters are used to factory the route URL, in some cases lazily (by returning a proc object), since some APIs need to access the method arguments early to compose the URL.
53
+
54
+ #### Paginator
55
+ Thron APIs that return a list of results are limited to a maximum of **50**.
56
+ To avoid repeating the same call many times by passing an augmented offset,
57
+ is possible to call a wrapper method on the gateway objects that returns a paginator object.
58
+ Once the paginator is loaded, it allows to navigate the results by using the following interface:
59
+ * **next**: loads the first offset and move forward, returning last when max offset is reached
60
+ * **prev**: move backwards from the current offset, returning first when minimum offset is reached
61
+ * **preload**: does not move the offset, but indeed preload the specified number of
62
+ data by performing an asynchronous call (wrapped in a thread).
63
+
64
+ Paginator keeps an internal cache to avoid hitting the remote service more than
65
+ once. The same is used when preloading results. Keep that in mind when you need to get fresh data.
66
+
67
+ ### Response
68
+ The HTTParty response has been wrapped in order to return a logical object that wraps the APIs return values.
69
+ The main attributes are:
70
+ * http_code: the HTTP code of the response
71
+ * body: the body of the response, in case the result is JSON data, it contains the data parsed into an appropriate entity (read below)
72
+ * total: in case the API returns a list of results, it indicates the total number of records; it's used by paginator
73
+ * error: the error message, if any, returned by the API
74
+
75
+ ### Dynamic Entities
76
+ Some of the Thron APIs return a JSON representation of an entity. I have initially
77
+ considered wrapping each entity into its own PORO object, but gave up after few
78
+ days since the quality of the returned entities is very large.
79
+ I opted instead to wrap returned data into a sub-class of the OpenStruct object, having the same
80
+ dynamic behaviour while adding some sugar features:
81
+ * it converts the lower-camel-case parameters into (more rubesque) snake-case
82
+ * it does recursive mapping of nested attributes
83
+ * it converts time and date values to appropriate Ruby objects
84
+ The same object can be used when passing complex parameters to some of the APIs: in this case is sufficent to call the *#to_payload* method on the entity
85
+ to convert the object in an hash with lower-camle-case keys (see examples below).
86
+
87
+ ### User Aggregate
88
+ To avoid accessing the multitude of Thron APIs via several objects, an aggregate has been created (DDD anyone?).
89
+ The **User** aggregate delegates most of its methods directly to the gateway objects (it uses the *Forwardable* module).
90
+ It keeps a registry of the gateway objects in order to refresh them on login: this
91
+ is requested to update the token id that identifies the current session and that is
92
+ stored internally by the gateway objects.
93
+
94
+ To create the user aggregate simply instantiate it without arguments:
95
+ ```ruby
96
+ user = Thron::User::new
97
+ ```
98
+
99
+ ## Interface
100
+ The following examples illustrates how to use this library.
101
+ Thron APIs include a broad range of methods, for a complete list of them please consult the [official APIs documentation](https://developer.thron.com/index.php).
102
+ For the APIs that accepts composed arguments simply use the dynamic entity described
103
+ above: just be aware to name its attributes by snake-case and not as the
104
+ lower-camel-case specified in the Thron documentation.
105
+
106
+ ### Access
107
+ To get a valid session token just login with your credentials:
108
+ ```ruby
109
+ user.login(username: '<your_username>', password: '<your_password>')
110
+ ```
111
+ From here you can check current user with the available methods, for example:
112
+ ```ruby
113
+ user.validate_token
114
+ ```
115
+ Or you can query the APIs to return other details:
116
+ ```ruby
117
+ user.user_detail(username: '<a_username>')
118
+ ```
119
+ Uploads the avatar image for a user (it relies on Linux *file* system call):
120
+ ```ruby
121
+ avatar = Thron::Entity::Image::new(path: '<path_to_an_image>').to_payload
122
+ user.update_image(username: '<a_username>', image: avatar)
123
+ ```
124
+
125
+ ### Contents
126
+ Thron is all about managing users contents, so no surprise there is a plethora of
127
+ methods at your disposal:
128
+
129
+ Find the contents by using the paginator object:
130
+ ```ruby
131
+ paginator = user.find_contents_paginator
132
+ paginator.preload(10) # preload the first 10 calls
133
+ paginator.next # fetch first result set from preloaded cache
134
+ ```
135
+ Show the contents by category (slightly more efficient):
136
+ ```ruby
137
+ user.show_contents(category_id: '<a_category_id>')
138
+ ```
139
+ Load specific content detail:
140
+ ```ruby
141
+ user.content_detail(content_id: '<a_content_id>')
142
+ ```
143
+
144
+ ### Groups
145
+ Users are arranged into different groups.
146
+
147
+ Create a new group:
148
+ ```ruby
149
+ group = Thron::Entity::Base::new(active: true, name: 'my new group').to_payload
150
+ user.create_group(data: group)
151
+ ```
152
+ List existing groups:
153
+ ```ruby
154
+ paginator = user.find_gropus
155
+ paginator.next
156
+ ```
157
+
158
+ ### Categories
159
+ Thron contents are organized by categories.
160
+
161
+ List existing categories (without paginator):
162
+ ```ruby
163
+ user.find_categories
164
+ ```
165
+ Create a new locale for a category:
166
+ ```ruby
167
+ locale = Thron::Entity::Base::new(name: 'photos', description: 'JPG and PNG images', locale: 'EN').to_payload
168
+ user.create_category_locale(category_id: '<a_category_id>', locale: locale)
169
+ ```
170
+
171
+ ### Disguise
172
+ Thron APIs allow to disguise another user via its apps sub-system.
173
+
174
+ Disguising only works inside the block:
175
+ ```ruby
176
+ user.disguise(app_id: '<app_id_that_can_disguise>', username: '<username_to_disguise>') do
177
+ # load the disguised user contents, each gateway will now use the disguised token id
178
+ contents = user.find_contents
179
+ # do something with contents
180
+ end
181
+ # finished disguising, it returns disguised token id
182
+ ```
@@ -0,0 +1,10 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new(:spec) do |t|
5
+ t.libs << 'spec'
6
+ t.libs << 'lib'
7
+ t.test_files = FileList['spec/**/*_spec.rb']
8
+ end
9
+
10
+ task :default => :spec
@@ -0,0 +1,80 @@
1
+ # -*- mode: ruby -*-
2
+ # vi: set ft=ruby :
3
+
4
+ # All Vagrant configuration is done below. The "2" in Vagrant.configure
5
+ # configures the configuration version (we support older styles for
6
+ # backwards compatibility). Please don't change it unless you know what
7
+ # you're doing.
8
+ Vagrant.configure(2) do |config|
9
+ # The most common configuration options are documented and commented below.
10
+ # For a complete reference, please see the online documentation at
11
+ # https://docs.vagrantup.com.
12
+
13
+ # Every Vagrant development environment requires a box. You can search for
14
+ # boxes at https://atlas.hashicorp.com/search.
15
+ config.vm.box = "ubuntu/trusty64"
16
+
17
+ # Disable automatic box update checking. If you disable this, then
18
+ # boxes will only be checked for updates when the user runs
19
+ # `vagrant box outdated`. This is not recommended.
20
+ config.vm.box_check_update = false
21
+
22
+ # Create a forwarded port mapping which allows access to a specific port
23
+ # within the machine from a port on the host machine. In the example below,
24
+ # accessing "localhost:8080" will access port 80 on the guest machine.
25
+ # config.vm.network "forwarded_port", guest: 80, host: 8080
26
+
27
+ # Create a private network, which allows host-only access to the machine
28
+ # using a specific IP.
29
+ config.vm.network "private_network", ip: "192.168.33.22"
30
+
31
+ # Create a public network, which generally matched to bridged network.
32
+ # Bridged networks make the machine appear as another physical device on
33
+ # your network.
34
+ # config.vm.network "public_network"
35
+
36
+ # Share an additional folder to the guest VM. The first argument is
37
+ # the path on the host to the actual folder. The second argument is
38
+ # the path on the guest to mount the folder. And the optional third
39
+ # argument is a set of non-required options.
40
+ # config.vm.synced_folder "../data", "/vagrant_data"
41
+
42
+ # Provider-specific configuration so you can fine-tune various
43
+ # backing providers for Vagrant. These expose provider-specific options.
44
+ # Example for VirtualBox:
45
+ #
46
+ config.vm.provider "virtualbox" do |vb|
47
+ # Display the VirtualBox GUI when booting the machine
48
+ #vb.gui = true
49
+
50
+ # Customize the amount of memory on the VM:
51
+ vb.memory = "6144"
52
+ vb.cpus = 3
53
+ end
54
+ #
55
+ # View the documentation for the provider you are using for more
56
+ # information on available options.
57
+
58
+ # Define a Vagrant Push strategy for pushing to Atlas. Other push strategies
59
+ # such as FTP and Heroku are also available. See the documentation at
60
+ # https://docs.vagrantup.com/v2/push/atlas.html for more information.
61
+ # config.push.define "atlas" do |push|
62
+ # push.app = "YOUR_ATLAS_USERNAME/YOUR_APPLICATION_NAME"
63
+ # end
64
+
65
+ # Enable provisioning with a shell script. Additional provisioners such as
66
+ # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the
67
+ # documentation for more information about their specific syntax and use.
68
+ # config.vm.provision "shell", inline: <<-SHELL
69
+ # sudo apt-get update
70
+ # sudo apt-get install -y apache2
71
+ # SHELL
72
+ $script = [
73
+ "sudo apt-add-repository ppa:brightbox/ruby-ng",
74
+ "sudo apt-get update",
75
+ "sudo apt-get -y -q install build-essential libssl-dev git",
76
+ "sudo apt-get -y -q install ruby2.3 ruby2.3-dev",
77
+ "sudo gem install bundler"]
78
+
79
+ config.vm.provision "shell", inline: $script.join(" && ")
80
+ end
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "thron"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,10 @@
1
+ logger:
2
+ level: <%= ENV.fetch('LOGGER_LEVEL') { 'warn' } %>
3
+ verbose: <%= ENV.fetch('VERBOSE') { false } %>
4
+
5
+ circuit_breaker:
6
+ threshold: <%= ENV.fetch('CIRCUIT_BREAKER_THRESHOLD') { 0 } %>
7
+
8
+ thron:
9
+ client_id: <%= ENV.fetch('THRON_CLIENT_ID') { 'none' } %>
10
+ protocol: <%= ENV.fetch('THRON_PROTOCOL') { 'https' } %>
@@ -0,0 +1,3 @@
1
+ require 'thron/version'
2
+ require 'thron/root'
3
+ require 'thron/user'
@@ -0,0 +1,46 @@
1
+ module Thron
2
+ class CircuitBreaker
3
+ class OpenError < StandardError; end
4
+
5
+ %w[open closed].each do |name|
6
+ const_set(name.upcase.to_sym, name.to_sym)
7
+ end
8
+
9
+ def initialize(options = {})
10
+ @state = CLOSED
11
+ @threshold = options.fetch(:threshold) { 5 }
12
+ @error_count = 0
13
+ @ignored = options[:ignored].to_a
14
+ end
15
+
16
+ def monitor
17
+ return yield if @threshold.zero?
18
+ fail OpenError, 'the circuit breaker is open!' if open?
19
+ result = yield
20
+ handle_success
21
+ result
22
+ rescue OpenError
23
+ raise
24
+ rescue => error
25
+ handle_error(error) unless @ignored.include?(error.class)
26
+ raise
27
+ end
28
+
29
+ def open?
30
+ @state == OPEN
31
+ end
32
+
33
+ private
34
+
35
+ def handle_success
36
+ @error_count = 0
37
+ end
38
+
39
+ def handle_error(error)
40
+ @error_count += 1
41
+ if @error_count >= @threshold
42
+ @state = OPEN
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,35 @@
1
+ require 'yaml'
2
+ require 'erb'
3
+ require 'dotenv'
4
+ require 'ostruct'
5
+ require 'logger'
6
+ require 'thron/root'
7
+
8
+ module Thron
9
+ module Config
10
+ extend self
11
+
12
+ CONFIG_YML = Thron::root.join('config', 'thron.yml')
13
+
14
+ def dump_yaml
15
+ Dotenv.load
16
+ @yaml ||= YAML.load(ERB.new(File.read(CONFIG_YML)).result)
17
+ end
18
+
19
+ def logger
20
+ @logger ||= begin
21
+ level = dump_yaml.fetch('logger').fetch('level')
22
+ verbose = dump_yaml.fetch('logger').fetch('verbose')
23
+ OpenStruct.new(level: Logger::const_get(level.upcase), verbose: verbose)
24
+ end
25
+ end
26
+
27
+ def circuit_breaker
28
+ @circuit_breaker ||= OpenStruct.new(dump_yaml['circuit_breaker'])
29
+ end
30
+
31
+ def thron
32
+ @thron ||= OpenStruct.new(dump_yaml['thron'])
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,78 @@
1
+ require 'ostruct'
2
+ require 'time'
3
+ require 'thron/string_extensions'
4
+
5
+ module Thron
6
+ using StringExtensions
7
+ module Entity
8
+ class Base < OpenStruct
9
+ TIME_REGEX = /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.+/
10
+ DATE_REGEX = /\A\d{4}-\d{2}-\d{2}/
11
+
12
+ def self.factory(args)
13
+ case args
14
+ when Hash
15
+ new(args)
16
+ when Array
17
+ args.map { |data| new(data) }
18
+ end
19
+ end
20
+
21
+ def initialize(hash = {})
22
+ @table = {}
23
+ hash.each do |k,v|
24
+ k = k.to_s.snakecase.to_sym
25
+ @table[k] = case v
26
+ when Hash
27
+ self.class.new(v)
28
+ when Array
29
+ if v.first.is_a?(Hash)
30
+ v.map { |e| self.class.new(e) }
31
+ else
32
+ v
33
+ end
34
+ when TIME_REGEX
35
+ Time::parse(v)
36
+ when DATE_REGEX
37
+ Date::parse(v)
38
+ else
39
+ v
40
+ end
41
+ new_ostruct_member(k)
42
+ end
43
+ end
44
+
45
+ def to_h
46
+ self.each_pair.reduce({}) do |acc, (k,v)|
47
+ acc[k] = fetch_value(v, :to_h); acc
48
+ end
49
+ end
50
+
51
+ def to_payload
52
+ self.each_pair.reduce({}) do |acc, (k,v)|
53
+ k = k.to_s.camelize_low
54
+ acc[k] = fetch_value(v, :to_payload); acc
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def fetch_value(value, message)
61
+ case value
62
+ when Base
63
+ value.send(message)
64
+ when Array
65
+ if value.first.is_a?(Base)
66
+ value.map { |entity| entity.send(message) }
67
+ else
68
+ value
69
+ end
70
+ when Date, Time
71
+ value.iso8601
72
+ else
73
+ value
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end