Tumgik
aritylabs · 9 years
Text
Retour sur un an sans variables d’instances, rappel sur l’encapsulation en Ruby
Il y a maintenant un an que je n’ai pas utilisé de variable d’instance en Ruby1 ! Dans cet article je reviens sur les raisons, les avantages et les inconvénients de cette drôle de privation !
Tout a commencé au détour d’une très intéressante lecture. Celle du livre de Sandi Metz : Practical Object Oriented Design in Ruby (POODR pour les intimes). Dans le premier chapitre, Sandi recommande d’envelopper en toute circonstance les variables d’instance dans des méthodes pour écrire du code qui embrasse le changement.
Tumblr media
Je cite :
« Always wrap instance variables in accessor methods instead of directly referring to variables. Hide the variables, even from the classes that defines them, by wraping them in methods. »
Bien que les exemples donnés par l’auteur dans son livre ne vont pas loin (tout comme ceux de cet article !), ces mots ont sonné comme une évidence et j’ai tout de suite adopté cette recommandation.
Rappel sur l’encapsulation en Ruby
L’encapsulation est le fait de cacher la représentation interne des données d’un objet au monde extérieur. Des ouvertures peuvent être concédées sur les mécanismes internes de l’objet, dans ce qui représente l’interface publique. Réduire cette interface au strict nécessaire réduit la complexité du système et augmente sa robustesse en découplant ses composants les uns les autres.
On peut dire que Ruby force l’encapsulation en ne disposant pas d’attributs comme dans certains langages orientés objet (pas de variables attachées aux objets d’instances). Les données internes de l’objet sont disposées dans des variables d’instances accessibles uniquement de l’intérieur2.
product.rb
Pour exposer ces variables, des méthodes doivent être créées :
product.rb
Mais des accesseurs peuvent être plus simplement créés via les macros  attr_accessor (qui crée un setter et un getter) ou plus spécifiquement attr_reader, attr_writer.
product.rb
L’idée principale de cet article est que rien ne nous limite à utiliser ces accesseurs pour exposer publiquement des variables d’instance. Si une méthode attr_* est appelée après private, les accesseurs seront privés.
product.rb
La recommandation de Sandi peut donc être librement reformulée ainsi :
« Utilisez des accesseurs privés au lieu de faire référence directement à des variables d’instance, ça réduira le coût de vos changements futurs. »
Voici mon retour d’expérience par rapport à ce code-style :
(+) Avantage : évolutivité du code
C’est l’argument que met en avant Sandi. Encapsuler les variables d’instance permet d’altérer l’accès ou l’écriture de l’information sans devoir modifier toutes les occurrences où est utilisé la variable d’instance. Cela peut être :
Une logique supplémentaire :
Comme pour multiplier par 100 une valeur décimale afin d’enregistrer la valeur dans un entier et éviter les déboires des virgules flottantes :
product.rb
Une mémoïsation
Pour mémoriser le résultat d’un calcul coûteux ou un appel d’API, ou réutiliser une valeur qui ne doit pas changer. L’opérateur-méthode ||= est habituellement utilisé pour faire une assignation seulement si le receveur de la valeur est nil
product.rb
Une valeur par défaut
On peut décider ultérieurement de suppléer une valeur par défaut aux cas où une valeur n’est pas passée
product.rb
Une conversion de type
On peut également ajouter un cast de type lors de la lecture ou de l’écriture : 
product.rb
Une validation
Ou ajouter des restrictions sous forme d’exception si la valeur écrite n’est pas valide.
product.rb
(+) Avantage : visibilité des attributs d’une classe
L’un des inconvénients des variables d’instances est qu’elles n’ont pas à être déclarées. Vous en voulez une, utilisez-la quelque part sur la méthode qui vous plait ! Il devient donc difficile de se faire une idée rapide de la représentation interne des données d’une classe. En revanche, les accesseurs sont des méthodes, qui peuvent ressortir sur les générations de documentations et les diagrammes de classes3.
J’ai pris l’habitude de déclarer mes accesseurs autour du premier `private` de la classe, les accesseurs publics vont devant ce mot clé, les autres après. Vous pouvez les déclarer au début de votre classe, mais vous devriez avoir un appel à public après vos accesseurs privés !
product.rb
D’ailleurs c’est pour cette raison que je recommande d’écrire l’appel de la méthode d’accesseur même si vous allez redéfinir et le setter et le getter pour l’un des motifs décrits ci-dessus ou pour d’autres.
(+) Avantage : plus d’intérêt pour l’interface publique de la classe
Devoir déclarer les accesseurs d’une classe dès le début amorce assez tôt la réflexion sur son interface publique. Si vous posez d’emblée le mot clé private pour écrire un accesseur, les méthodes se répartiront naturellement avant ou après le private et ses accesseurs.
(+) Avantage : instantiation facile et sécurisée via Hash parameter
Le constructeur d’une classe reçoit habituellement un nombre de paramètres qu’il case dans des variables d’instance. Avec des accesseurs, cette tâche peut être simplifiée :
(+) Avantage : pas de risque de mauvaise orthographe
Les classes Ruby sont peu regardantes vis à vis des variables d’instance. Si vous tentez de lire une variable inexistante directement (sans accessor), vous aurez nil en retour et des bugs silencieux potentiels ! C’est différent pour les accesseurs qui cassent tout à la moindre mauvaise orthographe sur leurs noms.
(-) Inconvénient : ce n’est pas toujours justifié
Une classe est habituellement écrite dans un seul fichier et peut ne faire qu’un écran de lignes de code. Envelopper une variable d’instance qui n’est probablement utilisée qu’une seule fois dans des accesseurs peut être vu par certains comme de la sur-ingénierie.
(-) Inconvénient : moins de gaité dans la coloration syntaxique
Se passer des variables d’instance signifie une (belle) couleur de moins sur votre éditeur de texte. Outre l’aspect purement esthétique, il devient difficile de distinguer les variables d’instances des variables locales ou des paramètres de méthodes. L’écriture dans les accesseurs sera néanmoins facilement reconnaissable étant donné qu’elle nécessite d’être précédée par self.
(-) Inconvénient : devoir écrire des accesseurs quand on n’en a pas envie
Il arrive qu’on se lasse de la routinière tâche de créer des classes et déclarer des accesseurs. On n’a qu’une envie, utiliser une variable d’instance et ne pas culpabiliser. Bein, faites-le, vous n’en brûlerez pas en enfer !
One last think : virer les variables d’instances des views Ruby on Rails
Cette recommandation peut être appliquée à la communication contrôleur / vue dans les applications Ruby on Rails.
products_controller.rb
La méthode de classe helper_method rend la méthode qui lui est passée en paramètre accessible depuis la vue en tant que helper
new.html.erb
Conclusion
Se prêter au changement est l’une des qualité les plus précieuses du code. Le livre de Sandi Metz donne beaucoup d’importance à l’évolutivité du code et je vous recommande vivrement de le (re)lire.
Notes
1 - en dehors des méthodes setter et getters bien entendu, comme nous verrons dans la suite de l’article 2 - les méthodes instance_variable_get et instance_variable_set peuvent être utilisées de l’extérieur mais ce n’est pas un moyen naturel de le faire, Ruby autorise de le faire mais il est clair que c’est une violation éhontée de l’encapsulation ! 3 - un diagramme de classes de vos applications Rails peut être généré railroady et graphviz
0 notes
aritylabs · 9 years
Text
Fake multitenant en Ruby on Rails : servir plusieurs clients avec une seule base de données
Dans un article précédent, je présente un guide exhaustif sur la multitenacité en Ruby on Rails. Mais des lecteurs m'ont souligné qu'une approche plus légère, à une seule base de données peut être suffisante pour les petits besoins.
Nous allons découvrir dans cet article les techniques qui permettent de séparer au mieux les données de plusieurs clients sur une base de données partagée. .
Tumblr media
TL;DR retrouvez sur Github une ruby gem où je regroupe toutes les techniques développées ici.
Chacune des tables de notre base de données doit faire référence au tenant grâce à une colonne `tenant`. Cela voudra dire que les clients devront partager le même compteur d'identifiant unique, ce qui gâche l'impression de multitenacité et peut amener les utilisateurs à se poser des questions sur la sécurité de leurs données. Nous allons donc nous intéresser plus particulièrement à ce point.
Remarque : cet article ne s'intéresse pas aux informations des tenants et du basculement du contexte à partir des domaines ou autres. Rapportez-vous à l'article sur la multitenacy pour avoir tous les détails nécessaires. Pour utiliser le code de l'article présent, un modèle Tenant doit exister et répondre à la méthode de class Tenant::current et la méthode d'instance Tenant#switch. Exemple minimaliste :
Créer un identifiant spécifique au tenant pour chaque table
En plus de l'identifiant (id) habituel des tables, nous ajouterons un id par scope. Autrement dit, un identifiant auto-incrémentiel dépendant de la colonne "tenant".
Nous pouvons faire cela manuellement pour chaque table :
Mais privilégions une approche plus transparente pour le développeur.
Altérer le mécanisme des migrations
Nous allons modifier le comportement par défaut des migrations afin que l'ajout de ces colonnes soit transparent pour le développeur.
D'après lecture du code d'ActiveRecord responsable des migrations, nous allons intervenir au niveau de la classe ActiveRecord::ConnectionAdapters::AbstractAdapter, utilisée par les migrations et la création de schémas de base de données à partir du fichier schema.rb.
Cette classe inclut le module SchemaStatements d'où provient la fameuse méthode create_table. Notre module MultitenantSchemaStatements redéfinira cette méthode tout en utilisant les mécanismes internes de l'ancienne méthode (via super).
L'id autoincrémenté sera renomé multitenant_id. L'ID par tenant prend le nom id et sera utilisé comme clée primaire illusoire par le modèle. Son autoincrémentation devra être gérée manuellement, par défaut de solution native au SGBD.
Notons l'espèce de "fusion de procs" que nous effectuons. create_table prend un block que nous capturons sous forme de proc dans la variable bloc. Right ?! Nous avons besoin d'ajouter d'autres instructions pour les colonnes :id et :string. Nous passons donc à super un nouveau block qui appelle à son tour le block originel si ce dernier est passé.
Si vous voulez que des modèles soient exclus de la multitenancité, passez à create_table l'option multitenant: false.
create_tenants.rb
Adapter le chargement de schémas
Les migrations ne sont pas le seul moyen de créer une base de données en Rails. C'est même le pire moyen de le faire. La base de données doit être créée à partir du fichier schema.rb et la commande rake db:schema:load.
schema.rb est mis à jour après les migrations et doit être versionné. Nous devons empêcher que nos colonnes de multitenacy y figurent ! Celles-ci étant créées implicitement par le nouveau module sus-décrit.
Cela se passe dans la classe ActiveRecord::SchemaDumper qui génère le code Ruby du fichier schema.rb. Cette classe n'est pas propice au monkey patching, l'omission intelligente des colonnes ne peut se faire sans la copie de pans entiers de code. Cela met en péril la durabilité de notre solution, mais nous n’avons pas le choix. Les passages affectés sont mis en surbrillance.
schema_dumper.rb
Pas idéal, mais suffisant. Petite consolation, SchemaDumper n’a pas évolué dans le code de Rails depuis 3 mois ;)
Testons maintenant le schema généré avec RSpec :
multitenancy_spec.rb
Fake::Multitenancy Schema dump should create table should not add primary key should not add extra columns should not add extra indexes
Finished in 0.18857 seconds (files took 0.64384 seconds to load) 4 examples, 0 failures, 1 pending
Gérer manuellement l'autoincrémentation contextuelle de `id`
À ce stade, nous disposons d'une colonne id qui n'accepte pas les valeurs nulles. Nous allons la remplir au moment de la création. Nous définirons le nom du tenant au passage, qui détermine le scope de l'identifiant. Et nous verrons plus tard que cela n'est pas nécessaire.
multitenancy.rb
Gérer les situations de compétition
Notre solution fait une requête supplémentaire au moment de la création de l'enregistrement pour obtenir le dernier id en date. Cela nous expose au risque de situation de compétition dans le cas où deux workers parallèles font cette requête à peu près au même moment.
Nous devons faire ajouter un verrou SQL qui rejette les identifiants doublons. Nous savons que les colonnes des tables peuvent prendre l'option unique: true, mais nous voulons un verrou sur deux colonnes, id et tenant.
Nous y arriverons via la définition d'un index, qui améliorera par ailleurs les performances des (nombreuses) requêtes impliquant ces deux colonnes :
Nous avons également ajouté des index individuels pour ces requêtes.
S'accrocher aux enregistrements doublons
Si une situation de compétition survient, le doublon sera rejeté et une exception ActiveRecord::RecordNotUnique soulevée. Mais nous pouvons sauver la mise et éviter la perte de données :
C'est dans la méthode save() que nous interviendrons. S'il s'agit d'une situation de création (!persisted?) sur un modèle multitenant?, trois (3) tentatives sont accordées à l'opération.
multitenancy.rb
Créer un scope global pour tous les modèles
Maintenant que nos données sont bien rangées par tenant et bien étiquetées par id, nous devons gérer leur lecture. Il est impératif d'éviter tout débordement de données entre un tenant et un autre. C'est pour cette raison que nous allons opter pour un scope global via default_scope.
base.rb
Au lieu de définir un modèle abstrait intermédiaire où sera définie self.default_scope, nous avons préféré créer ce scope au moment où une classe hérite de ActiveRecord::Base. self.inherited est un hook Ruby standard, similaire à Module::included ou le moins connu Module::extended.
Ce scope sera "hérité" par tous les modèles et sera actif pour tous les modèles ayant une colonne tenant. Il sera utilisé partout, dans les requêtes where, dans les count, dans les associations et même dans les créations et les initializations. Exemple :
> Tenant.find('best_client_event').switch > Product.new.tenant => 'best_client_event'
L'assignation du tenant dans le before_create devient donc superflue, mais gardons la quand même !
Il faut attacher un soin particulier —si vous êtes amenés à définir un défault_scope— à y appeler super.
Exemple pour exclure les utilisateurs bannis de toutes les requêtes :
class User   has_many :articles   def self.default_scope     super.where(banned: false)   end end > User.all.to_sql select * from `users` where `tenant` = 'best_client_event'` AND `banned` = 'f'
D'ailleurs vous devriez fuire cette feature de ActiveRecord comme la peste. Le fake multitenant est d'expérience le seul cas d'usage qui justifie son utilisation.
En effet, default_scope peut avoir des effets inattendus sur l'intégrité référentielle de vos associations. Pour les contrecarrer, votre code devra être parsemé d'appels .unscoped. Pas pratique, car vous auriez surement songé à utiliser default_scope pour écrire moins de scopes dans votre code !
Reprenons le dernier exemple :
class Article   belongs_to :author, class: User end
Si l'auteur d'un article est banni et que ses articles restent disponibles, l'association Article#author retournera nil pour un user pourtant existant.
@article.author.fullname soulèvera dès lors une exception de méthodes inconnu sur nil ! Essayons :
user = User.create(fullname: 'Stephen King') user.articles << (article = Article.create()) article.author.fullname # => 'Stephen King' user.banned = true user.save article.author.fullname # => NoMethodError: undefined method `fullname' for nil:NilClass
Cet excès de zèle est cependant le bienvenu pour le fake multitenant ! Nous avons besoin d'une solution très stricte et nous voilà servis.
Derniers garde fous
default_scope est inopérant pour les requêtes construites via Model.find_by_sql. Nous ajouterons un avertissement dans le cas où il est utilisé.
multitenancy.rb
Conclusion
Nous avons utilisé des techniques simples et accessibles pour servir plusieurs clients avec une seule base de données, tout en donnant l'impression que les données de chacun sont sur une base de donnée séparée.
Nous avons également vu :
Que default_scope est une fausse bonne idée. En pratique, son utilisation revient à se tirer un autogoal. Réaliser la multitenacité est cependant l'une des rares indications de son utilisation
La création d'une base de données doit passer par un fichier schema.rb à jour et versionné et non par le déroulement de toutes les migrations. Les migrations sont faites pour faire évoluer une base de données existante.
Les méthodes longues comme table() de SchemaDumper empêche la surcharge intelligente de code. Si cela peut être toléré dans le code d'une appli, sa présence dans un framework de la taille de Rails est clairement une faute de design.
Ruby appelle la méthode de classe self.inherited quand un classe a une nouvelle sous-classe (par héritage). Nous avons utilisé cette propriété du langage pour définir des scopes pour tous nos modèles multitenants.
Merci d'avoir pris le temps de me lire (:
0 notes
aritylabs · 9 years
Text
Moteur de template magique en Ruby, rencontre avec les Bindings !
Dans cette expérimentation, nous allons créer un simple moteur de template en nous efforçant de simplifier son utilisation à l’extrême.
Nous rencontrerons les binding en Ruby et nous en ferons une utilisation pratique !
Tumblr media
TL;DR l'ensemble du code de cet article a été compilé en une Rubygem disponible sur Github.
Notre moteur servira à remplacer les expressions entre {{doubles.accolades}} d'un texte par des valeurs. Ces expressions peuvent être un ou plusieurs appels de méthodes successifs. Notre moteur fera cette interpolation à partir d’un contexte qui peut être n’importe quel objet. (Voir les détails de l'implémentation dans les commentaires du code :)
tiny_template.rb
Le constructeur de la classe TinyTemplate prend une chaine qui contient des accolades à interpoler et sa méthode parse reçoit le contexte. On peut l’utiliser de cette manière :
greeting.rb
> Greeting.new "Hello JOHN DOE and welcome to My Fancy Website, your email is [email protected]"
Contrairement à l'interpolation standard en Ruby, notre chaine est une valeur et non du code. Nous pouvons l'enregistrer dans une base de données et laisser l'utilisateur la modifier sans s'exposer à des risques de sécurité. Seules les méthodes publiques et qui ne prennent pas d'arguments peuvent être appelées en chaine.
Se passer du constructeur avec une méthode de classe
Nous pouvons simplifier cette utilisation en passant par une méthode de classe :
tiny_template.rb
Cette méthode de classe instancie TinyTemplate et appelle la méthode d’instance du même nom sur cette nouvelle instance.
greeting.rb
Se passer de parse grâce à une fonction de module
tiny_template.rb
Nous venons de définir une méthode TinyTemplate dans un module inclus en dehors de toute classe ou module. Cela revient à inclure ce module dans Object et rendre ses méthodes universellement accessibles. Nous pouvons dès lors faire :
greeting.rb
Se passer du contexte grâce à la machine à remonter dans le binding !
Nous aimerions rendre le paramètre context optionnel, ou plutôt lui attribuer par défaut la valeur self s'il n'est pas défini. Or, self à l'intérieur de la méthode TinyTemplate(), ou même à sa lisière dans la définition def TinyTemplate(str, context = self) est différent de celui qu'on retrouve à l'extérieur, au niveau de l'appelant.
La valeur de self et de toutes les variables locales définies dans le contexte, de même que toutes les variables de classe ou d'instances à un point donné du code est ce qu'on appelle un binding.
Rien de bien neuf, hein ? Sauf que Ruby permet de capturer le binding à n'importe quel point du code. Il permet même de le passer à d'autres contextes qui peuvent l'examiner et en extraire des données.
La méthode binding de Kernel, accessible dans tous les objets et dans tous les contextes, permet de capturer le contexte en cours dans une instance de la classe Binding.
Cette classe n'offre cependant aucun moyen de lister ou inspecter les éléments de ce contexte. Elle permet en revanche d'y évaluer (exécuter) une chaine de caractère contenant du code Ruby via eval(). En effet, eval prend un deuxième paramètre qui n'est autre que le binding sur lequel sera faite l'évaluation.
Essayons :
discover_binding.rb
Nous avons dit que la classe Binding ne permet pas de lister les variables d'un contexte donné, mais rien ne nous empêche de le faire de l'extérieur grâce à une série d'eval()s sur les méthodes local_variables, instance_variables et global_variables ! Ces méthodes peuvent être appelées de n'importe quel point de code, pourquoi pas sur un binding capturé ? Essayons :
discover_binding.rb
Au passage, soulignons que eval() prend un troisième et un quatrième paramètre qui sont respectivement le nom du fichier et le numéro de ligne qui seront rapportés. L'usage est d'y passer les constantes "magiques" __FILE__ et __LINE__, ce qui permettra de guider le débogueur vers l'endroit de l'évaluation, en cas d'erreur de syntaxe.
D'après ce qu'on vient d'apprendre, nous pouvons passer le binding de l'appelant à TinyTemplate et utiliser ce binding pour évaluer self.
Et l'appeler ainsi : TinyTemplate(self, binding).
Sauf que cela ne nous a été d'aucune utilité. On a remplacé la variable context par caller_binding et on a dû ajouter un traitement supplémentaire sans rendre optionnel le paramètre context. Il nous faut un moyen d'accéder aux bindings précédents.
Ruby 2 et plus précisément MRI, l'implémentation officielle de Ruby (oui, car il y'en a plusieurs !) a rendu cela possible. Son API debug_inspector peut être utilisée pour remonter dans la pile des bindings précédents.
Rendons le paramètre context de TinyTemplate() optionnel. Dans le cas où il est omis, nous retrouvons le contexte de l’appelant à l'aide de RubyVM::DebugInspector.
tiny_template.rb
À ce point nous pouvons faire :
greeting.rb
Les bindings sont rarement manipulés explicitement en production. Mais la réalité est qu'ils sont partout utilisés dans nos codes. Les blocks Ruby et autres éléments de langages exécutables (procs et lambas) capturent automatiquement le binding où ils sont créés. Ce qui en fait des closures, des fermetures en bon français. L'exécution de leur code se fait dans un mix entre ce binding capturé automatiquement et celui où ils sont exécutés via .call ou yield.
Le binding d'un proc peut être accédé comme ça : proc{}.binding. Cette syntaxe peut notamment servir à accéder à une information située à l'extérieur d'un block passé à instance_eval. Mais ce n'est pas là notre sujet. Reprenons maintenant l'expérimentation :
Se débarrasser de “TinyTemplate” grâce aux open classes
Aurions-nous le moyen de raccourcir davantage cette syntaxe ? Oui, en utilisant un objet existant, qui n’est autre que la chaine de caractère du template à parser !
tiny_template.rb
En effet, Ruby nous permet d’ouvrir des classes existantes et d’y ajouter des méthodes : 
greeting.rb
Attendez, pour le coup on a dû rétablir .parse, mais pas pour longtemps : 
Se (re)débarasser de parse grâce à une méthode-opérateur
.parse c’est six caractères, il y a moyen de les réduire à un (1) seul, qui se placera devant la chaine à parser !
tiny_template.rb
Nous avons aliasé “~” (le caractère tilde, qui sert généralement à inverser le bit logique ou matcher des expressions régulières) à parse. Oui, c’est possible, car en Ruby, les opérateurs ne sont que des méthodes. Cela peut être fait également pour +, -, /, *, !, |, & et tous les autres opérateurs. Le tilde est un opérateur unaire, il ne prend qu'un seul paramètre, qui se trouve être self dans notre exemple.
Voici donc l'utilisation définitive :
greeting.rb
Conclusion
Je suis particulièrement content d'avoir pu parler des binding sur ce blog, car il est difficile de trouver un exemple real-life utile de leur utilisation directe. On n'a habituellement pas besoin de s'en préoccuper. C'est les outils de débogage et d'introspection de code comme pry ou better errors qui en font le plus usage.
Cet exercice est aussi une bonne manière de démontrer la puissance du langage Ruby, mieux en tout cas que les 2 + 3 qui donne 42, et autres 3.times{ puts “text” } ou 1.year.from_now !
0 notes
aritylabs · 9 years
Text
Multi-upload asynchrone et élégant avec Dropzone.js et Ruby on Rails
Dropzone.js est actuellement l’un des meilleurs outils JavaScript pour l’upload simultané de plusieurs fichiers. Il offre une expérience utilisateur réussie et fortement personnalisable grâce à une riche API.
Nous allons découvrir comment l’intégrer de manière satisfaisante et générique à nos applications Ruby on Rails.
Tumblr media
Installation
Via Rubygems
Dropzone peut être intégré à votre application via le gem dropzonejs-rails. Il suffit de l’ajouter au Gemfile et intégrer les assets aux fichiers application.js et application.css
Gemfile application.js application.css
Cet asset-gem n’a cependant aucune autre utilité. Il se contente d’encapsuler le code JS de Dropzone et n’offre aucun helper susceptible de nous aider, inutile donc de l’avoir comme dépendance Ruby :
Via Bower
Utiliser bower-rails pour gérer ses dépendances en assets est une bien meilleure idée. Cet outil repose sur un Bowerfile (pensez Gemfile !) très simple d’usage :
Ajouter une ligne à ce fichier :
Bowerfile
Lancer la commande d’installation :
> bundle exec rake bower:install
Ajouter l’emplacement des assets de bower dans le chemin de precompilation de l’application :
application.rb
L’application
Le multi-upload fait plus de sens lors d’une association, comme un produit qui peut avoir plusieurs photos :
product.rb picture.rb
L’upload des photos se fera à partir du formulaire de création et d’édition des produits, c’est pourquoi nous y autorisons les attributs de photos via `accepts_nested_attributes_for`
La classe Picture utilise Paperclip, la célèbre gem d'upload de fichiers. Nous y avons ajouté le style "dropzone" aux dimensions adéquantes (120x120). Ce format sera utilisé pour la visualisation des images par DropzoneJS.
Éditons maintenant le formulaire
Commençons par y placer une simple <div> :
_form.html.erb
Et lançons Dropzone dessus depuis un fichier JavaScript séparé (dropzone/integration.js) :
dropzone/integration.js application.js
Nous déclenchons la reconnaissance des dropzones dans un event ready et page:load pour être sûrs qu'elle fonctionne également avec en mode turbolinks introduits par Rails 4.
Le résultat ressemblera à cela, après l'ajout de quelques photos des ruines de Cuicul :
Tumblr media
Upload asynchrone dans des emplacements temporaires
Vous avez probablement remarqué que le paramètre `url`, qui correspond à l’emplacement où iront les fichiers uploadés n’est pas /pictures mais /temp_uploads.
La raison est qu’on n’a pas envie de persister les photos avant que soient persistées le produit (dans le cas d’une création) ou ses modifications dans le cas d’un update. D’ailleurs, pour intégrité référentielle la clé étrangère product_id de Picture peut tout simplement ne pas accepter de valeur NULL.
Voici le contrôleur qui recevra ces requêtes :
temp_uploads_controller routes.rb
Et voici TempUpload, une classe Ruby ordinaire qui enregistre le fichier uploadé dans un emplacement dédié du dossier tmp de l'application Rails.
D’ordinaire, ces fichiers temporaires doivent être supprimés lors de la création de l’enregistrement Picture à partir de notre TempUpload. Nous implémentons cela à partir d'un module TempUpload::Integration qui peut être inclus et utilisé par n'importe quel modèle
picture.rb integration.rb
Gérer la suppression de photos
Nous devons maintenant ajouter le moyen de supprimer des photos à partir du formulaire d’édition.
Mais repartons sur de bonnes bases et faisons évoluer notre solution en utilisant un Form Builder personnalisé, qui encapsulera toute la logique, relativement lourde, de génération :
dropzone_builder.rb
Ce qui saute tout de suite aux yeux est l'utilisation du block concerning sur toute la classe.
`concerning` est une méthode de la classe Module, introduite dès Rails 4.1. Elle permet de créer des concerns tout en les incluant dans la classe hôte. Son principal intérêt est de délimiter visuellement les différentes préoccupations d’un modèle.
Mais l'usage est là plus subtil : concerning nous permet d'utiliser notre builder de deux manières différentes:
Directement en tant que classe :
_form.html.erb
Ou indirectement dans un custom builder préalablement défini !
custom_builder.rb
Ce builder définit deux méthodes qui peuvent être utilisées dans nos formulaires : f.dropzone_field et son quasi-alias dropzone_images_field. Cette dernière appelle la première en forçant les mime-types acceptés aux seules images.
_form.html.erb
Mais ce builder ne génère ni HTML ni JavaScript. Nous déportons la logique de génération dans la classe DropzoneBuilder::Field appelée à la ligne 16 du builder :
dropzone_builder/field.rb
output génère deux balises <div>. l'une sera directement utilisée par Dropzone et l'autre recevra les différents champs hidden nécessaires à la prévisualisation Dropzone et à l'enregistrement back-end. (Retrouvez plus de détails sur le fonctionnement du code sur les commentaires.)
D'ailleurs, même cette classe Field ne fait pas tout le travail. Pour chaque image présente pour le produit en cours d'édition, des champs <input type="hidden"> doivent être créés. Nous confions cette génération à la classe DropzoneBuilder::Record instanciée dans Field à la ligne 19.
record.rb
Nous veillons à maintenir la trace des uploads temporaires en cas d'échec de soumission du formulaire. Si ce dernier contient des erreurs et ne passe pas la validation, les uploads sont repris à côté des images persistées (lignes 36 à 39). De même que nous évitons d'afficher les images supprimées dans le formulaire et nous maintenons leur attributs _destroy (lignes 44 à 48)
Nous aurions pu laisser un appel asynchrone récupérer la liste de photos d’un produit, mais tant que nous avons accès à cette information lors de la génération du formulaire, nous stockons les données nécessaires directement dans des balises <input type=hidden> qui seront de toute façon renvoyées côté serveur.
Intégration avec DropzoneJS proprement dite
Maintenant que toutes les données sont structurées dans le HTML, créons le JavaScript qui l'exploite :
integration.js
Nous y définissons RailsDropzone, un objet wrapper de Dropzone qui l'instancie selon les options stockées dans l'attribut data-dropzone de la balise <div> (ligne 17) et lui attache des events nécessaires. Dans l'event success qui se déclenche à la fin d'un upload asynchrone, RailsDropzone ajoute des champs hidden afin de transmettre l'identifiant du fichier temporaire au backend. removedfile se déclenche quant à lui au moment du clic sur le lien de suppression d'un fichier. Lorsqu'il s'agit d'un fichier fraichement uploadé (temporaire), une requête AJAX de type DELETE est lancée pour demander au controlleur TempUploads de le supprimer. (ligne 49)
Notez à la ligne 19 la lecture de la balise meta csrf-token afin de rendre l'insertion des images compatibles avec la protection anti-Cross-Site Request Forgery.
Dernières retouches
Internationalisation
Il ne nous reste qu’à internationaliser l’intégration grâce aux différents paramètres offerts par dropzone.js.
fr.yml field.rb
Nettoyer les uploads temporaires non-utilisés
L'utilisateur peut décider de ne pas soumettre le formulaire après avoir uploadé quelques photos. Ou quelque chose, comme une déconnexion, peut l'en empêcher. Des fichiers occuperont notre espace disque sans utilité.
Il convient de planifier une tâche qui supprime les fichiers temporaires ayant dépassé un certain délai. Nous n'allons pas détailler cette solution dans cet article. Si vous êtes intéressé de savoir comment planifier des tâches répétitives en Ruby on Rails, consultez cet article.
Interdire la soumission du formulaire pendant l'upload
Il convient également de désactiver le bouton de soumission du formulaire pendant l'upload.
Nous pouvons y arriver au prix de quelques évènements supplémentaires de Dropzone :
Conclusion
Nous sommes entrés dans les plus fins détails sordides de l'intégration du multi-upload asynchrone avec DropzoneJS dans une application Rails.
L'intégration superficielle de Dropzone est possible, mais peut affecter l'expérience utilisateur et provoquer perte de données et gaspillage de temps d'upload.
Annexe : pourquoi ne pas utiliser Tempfile de Ruby ?
La bibliothèque standard de Ruby inclut un mécanisme de fichiers temporaires mais qui n’est pas adéquat pour notre usage. La durée de vie d’un fichier Tempfile se termine au moment où son objet Ruby est ramassé par le garbage collector.
Nous pouvons cependant prolonger l’espérance de vie ce cet objet via hack.
En effet, la classe Tempfile définie à l’instanciation un finalizer qui n’est autre qu’un proc (bout de code variabilisé) qui s’exécutera au moment où la variable est éliminée de la mémoire par le ramasse-miette (garbage collector). Ce proc appelle unlink sur le fichier temporaire, le rendant introuvable.
Les finalizers sont les cousins très éloignés des destructeurs présents dans des langages comme C++ et C#. Vous pouvez les définir sur n’importe quel object, mais ils demeurent une fonctionnalité avancée de bas niveau à n’utiliser qu’à très bon escient. 
Dans notre exemple, prévenons la suppression en appelant undefine_finalizer sur notre objet, mais l‘interface de TempUpload reste inutilisable. Il ne nous sera pas possible de retrouver l’état de cette instance via TempUpload.find et nous seront donc obligés à manipuler directement le path du fichier temporaire. Ni pratique ni élégant.
0 notes
aritylabs · 9 years
Text
Internationaliser les enum Rails 4.1, faut-il utiliser les helpers ?
Rails 4.1 a introduit la feature tant demandée et attendue des enum, qui permet de limiter un attribut donné à quelques valeurs textuelles qu'il peut prendre.
Cette feature a tant tardé, car peu de systèmes de gestion de base de données ont un type natif pour cet usage. D'ailleurs la solution adoptée par Rails n'est qu'un artefact, car la valeur sera stockée dans un champ entier ordinaire, solution jugée la plus générique et la plus adéquate en terme de performances.
Mais un aspect a été délaissé, celui de l'internationalisation sur lequel nous allons nous pencher sur cet article.
Tumblr media
Enum, comment ça marche ?
Après avoir ajouté un champ integer à notre table via migration, nous déclarons les valeurs possibles au sein même du modèle. Dès lors, une panoplie de méthodes est créée pour assigner, tester et modifier notre enum.
Exemple : le service d'une réservation d'hôtel peut varier entre petit déjeuner, demi-pension, pension complète et all-inclusive.
add_service_to_bookings.rb booking.rb
Nous pouvons obtenir la valeur textuelle en appelant simplement booking.service, la modifier comme booking.service = :breakfast. Nous pouvons également vérifier s'il s'agit de l'une des valeurs en utilisant sa méthode dédiée booking.full_board? ou basculer et enregistrer la valeur en utilisant l'une des méthodes muatrices : booking.half_board!.
Notons au passage que la syntaxe %i{} est similaire à %w{} sauf qu'elle crée un tableau de symbols et non de strings. Ce petit sucre est une nouveauté Ruby 2.0
Internationalisation
Comme les valeurs que peut prendre notre enum sont utilisées pour créer des méthodes, nous ne pouvons pas y mettre ce qu'on veut. Cela ne représente pas un problème en anglais, car les noms de méthodes peuvent être simplement humanisées via la méthode #humanize définie par ActiveSupport.
> :full_board.to_s.humanize => "Full board"
Mais ce n'est pas le cas en français avec les caractères accentués et les éventuelles apostrophes. Ainsi, même une application à une seule langue nécessite l'internationalisation pour afficher correctement les valeurs des enum.
Première solution (primesautière) : les helpers !
Commençons par placer nos traductions dans l'emplacement adéquat. C'est cet emplacement qu'utilise Rails pour trouver les traductions des attributs pour la méthode #label.
fr.yml
Définissons ensuite deux helpers, enum_translate obtient la traduction d'une valeur à partir du nom du modèle et du nom de l'attribut enum. Cette méthode est utilisée par enum_select pour préparer la liste des valeurs et leurs traductions sur un champ select.
application_helper.rb _form.html.erb
Il y a too much hate à dire sur les helpers, mais commençons par souligner le smell qui veut que l'on préfixe nos méthodes et qu'on leur passe autant tant de paramètres pour obtenir une simple traduction !
De plus, les helpers ont essentiellement ça de rebutant : ils sont globalement disponibles dans toutes les views et sont appelés en apparence sans receiver. L'objet qui reçoit ces messages (aka. appels de méthodes) est donc self qui se trouve être une instance d'une classe anonyme qui hérite de ActionView::Base, qui compile et met en cache le code de la view. Cet objet est instancié par le contrôleur qui lui passe l'ensemble des variables d'instance définies dans l'action en cours et dans les filtres. C'est ce qui nous donne l'impression que la vue s'exécute au niveau du contrôleur et nous évite de passer explicitement des données entre l'action et la vue.
Remarquez également la différence fondamentale entre nos deux helpers. Le premier lit une valeur d'un fichier de configuration (fr.yml) et le deuxième génère du HTML. On comprend donc à l'évidence que les helpers ne sont qu'un four-tout à laisser au cas extrêmes.
Pour les helpers qui viennent avec Rails, ils peuvent être utilisés sans remords, mais pas tous. L'usage de ceux qui génèrent/manipulent/sécurisent du HTML/CSS/JS est tout à fait adéquat dans le contexte d'une view, mais d'autres comme pluralize (!) ou capitalize (!!) sont pour le moins déroutants.
Deuxième solution : nouvelle méthode dans le modèle
enum définie plusieurs méthodes, pourquoi pas une de plus pour obtenir les traductions ?
> booking.service_translated => "Pension complète"
L'implémentation peut ressembler à cela :
translated_enum.rb
D'après un regard indiscret sur le code source de la méthode ::enum, on découvre que les différentes méthodes générées pour chaque définition d'un enum sont proprement rangées dans un module séparé, accessible via ::_enum_methods_module.
Nous laissons la méthode originale de ActiveRecord faire le nécessaire (notez le super à la ligne 3) et nous marchons sur ces pas pour ajouter la méthode #enum_translated qui obtient la traduction nécessaire.
Pour les formulaires, nous aimerions également nous débarrasser du helper enum_select et avoir quelque chose de propre comme : 
_form.html.erb
form_form instancie ce qu'on appelle un form builder, et c'est là que nous définirons notre méthode #enum qui ne fait qu'appeler select() en générant au passage la liste des choix sans qu'on ait à les passer explicitement dans notre view.
form_builder.rb
Mais on peut encore faire mieux, laisser ActionView::Helpers::FormBuilder tranquile et définir un custom form builder où résidera la méthode enum : 
form_builder.rb
Nous pouvons soit le définir comme builder par défaut, et ce de manière globale dans application.rb
application.rb
Ou spécifier ce builder manuellement sur les appels de form_for : 
_form.html.erb
Troisième et meilleure solution : utiliser des chaines de caractère traduisibles !
La deuxième approche est nettement plus propre que celles des helpers, mais nous confions au modèle une responsabilité et des connaissances de présentation, ce qui est fâcheux !
Pourquoi ne pas considérer la valeur d'un attribut enum comme un objet spécial qui sait faire davantage que de simplement représenter un état ?
Faisons cela : 
value.rb value_extension.rb
Nous venons de définir une classe qui hérite de String mais qui encapsule des données supplémentaires qui sont le nom du modèle contenant notre enum et le nom de l'attribut de ce dernier. Nous remplaçons ensuite le comportement de la méthode de classe Model::enum pour renvoyer non pas des strings mais des instances de notre nouvelle classe Enum::Value.
Ça peut paraitre déconcertant qu'un object qui est en tout point identique à une chaine de caractère puisse contenir d'autres informations qui se trouvent n'être rien d'autres que des chaines de caractère ! Mais c'est ça Ruby !
Pour le moment, notre Enum::Value ne sait encore rien faire, attribuons-lui le pouvoir de s'auto-traduire. Faisons cela à partir du module I18n, histoire de bien ranger notre code !
translatable_enum_value.rb
Et voilà ! Nous pouvons à présent obtenir la traduction d'un attribut enum en appelant #translate ou son alias #t sur sa valeur.
> booking.service.translate => "Petit déjeuner"
Notre solution ne perturbe en rien le reste du code, car ces valeurs ressemblent et se comportent en tout point comme des Strings. En fait, ce sont des strings !
> booking.service.is_a? String => true
Conclusion
Le simple besoin de traduire les valeurs des enum nous a conduit à aborder plein de sujets, dont le bien fondé d'écrire des helpers et la création de form builders personnalisés. À bientôt sur un autre article et merci de me lire :)
1 note · View note
aritylabs · 9 years
Text
Sécuriser les appels de commandes système depuis Ruby 2.x, découverte des `refinements`
Il y a pire que les injections SQL, c'est les injections système. Si votre application doit interagir avec des outils en commande, vous encourez de graves danger si vous passez à vos commandes des paramètres issus de sources pas sûres, comme des données utilisateurs.
Dans cet article, nous verrons comment sécuriser l'exécution des commandes tout en évitant les effets secondaires grâce aux refinements.
Tumblr media
Privilégier les API et les couches d'abstraction
À cause de ces risques, il est préférable de communiquer avec les différents services via des API, même au sein d'un même système. C'est possible avec ElasticSearch ou Redis par exemple, qui font tout passer par des endpoint sous localhost.
Mais les outils qui proposent des API restent rares, préférez alors une couche d'abstraction en Ruby qui vous évite de préparer manuellement des commandes paramétrées. Et assurez-vous que cette couche gère de manière optimale la sanitization des paramètres. Si vous ne trouvez pas de couche d'abstraction, écrivez en une si possible, mais laissez la solution de préparer des commandes en dernier recours.
Appeler une commande système en Ruby, divers moyens nuancés
Il existe plusieurs moyens d'exécuter des commandes système en Ruby. S'ils semblent identiques ou interchangeables, il y a des nuances dans leur mode d'exécution et leurs valeurs de retour.
Le module Kernel, inclus dans Object et dont les méthodes sont par conséquent accessibles à tous les objets Ruby, inclut trois méthodes qui exécutent des commandes système : Kernel#`, Kernel#exec, Kernel#system.
1 - Kernel#` (remarquez le petit backstick après le dièse) exécute la commande dans un subshell (un processus enfant) et renvoie la sortie standard de la commande. Cette méthode est appelée via la syntaxe spéciale `commande` mais rien ne nous empêche de l'appeler plus conventionnellement via send.
> send('`', 'ls') => "Gemfile\nGemfile.lock\nREADME.rdoc\nRakefile\napp\nbin\nconfig\nconfig.ru\ndb\nlib\nlog\npublic\ntest\ntmp\nvendor\n"
C'est également cette méthode qui est appelée avec la syntaxe %x{commande} :
> %x{ls} => "Gemfile\nGemfile.lock\nREADME.rdoc\nRakefile\napp\nbin\nconfig\nconfig.ru\ndb\nlib\nlog\npublic\ntest\ntmp\nvendor\n"
2 - Kernel#system exécute la commande dans un subshell (comme `) mais renvoie le statut d'exécution, résumé en soit true soit false (ou nil pour les commandes inexistantes). L'exit-code de la dernière commande exécutée reste accessible dans la variable globale $? avant qu'il ne soit remplacé par le code de la prochaine commande système exécutée. system renvoie true uniquement si l'exit code de la commande est de 0.
> system('pwd') /Users/Ihcene/aritylabs/secure_shell => true
system se distingue également de ` par la possibilité de recevoir des variables d'environnement et des paramètres supplémentaires.
3 - Kernel#exec exécute la commande en remplaçant le processus actuel. L'exécution de votre code est donc interrompue pour de bon et il n'est pas possible de tester la valeur de retour ni d'inspecter les sorties standards. exec prend les mêmes paramètres que system.
Il existe d'autres moyens d'exécuter des commandes comme IO#popen et Open3#popen3. Ces derniers permettent un contrôle avancé des flux d'entrée de sortie et d'erreurs, mais nous nous intéresserons plus spécialement à Kernel#` pour la suite de cet article.
Sécuriser les commandes avec Shellwords
Sécuriser une commande est tr��s simple : il suffit d'échapper les paramètres de sources pas sûres à l'aide de Shellwords, un module de la bibliothèque standard de Ruby, qui respecte la norme POSIX / SUSv3.
> require 'shellwords' => true > params = {file: 'Gemfile ; rm -rf /'} > `grep rails #{Shellwords.escape( params[:file] )}` grep: Gemfile ; rm -rf /: No such file or directory => ""
Notons que require 'shellwords' ouvre la classe String pour y insérer des raccourcis commodes. Nous aurions pu faire :
> `grep rails #{ params[:file].shellescape )}`
Mais encore faut-il se souvenir d'échapper chaque paramètre ajouté de chaque appel de fonction. Un seul oublie et tout est compromis. C'est pourquoi nous allons élaborer une solution de tolérance zéro.
Suspecter toutes les commandes
Ouvrons d'abord la class String pour ajouter le moyen de reconnaitre si une chaine est sûre ou non. Utilisons ce moyen pour marquer les chaines échappées comme étant sûres.
secure_shell.rb
Maintenant, nous devons intervenir sur le module Kernel pour désigner qu'il n'accepte que les chaines échappées ou désignées explicitement comme sûres. Faisons celà par monkey patching classique. Le code peut sembler bizarre avec toutes les apostrophes inversées, mais il est pleinement fonctionnel.
secure_shell.rb
Nous allons à présent apporter quelques modifications de la classe String afin de permettre la création de chaines sûres par la concaténation d'autres chaines sûres. Pour cela, nous ferons du Monkey Patching d'une approche différente : au lieu de renommer et remplacer les méthodes voulues, nous prependront un module avec de nouvelles versions de nos méthodes : 
secure_shell.rb
Essayons pour voir : 
> ("grep rails".shell_safe! + params[:file].shellescape).shell_safe? => true
Mais notre solution n'est pas utilisable en l'état. La syntaxe du backstick `cmd` ne prend pas de variable et nous oblige de passer par des interpolations. Après des heures de recherches et de lecture de code C, j'ai conclu que l'interpolation de chaines en Ruby est effectuée en bas niveau sans appeler des méthodes surchargeables de la classe String ou autre. On est en effet en droit de croire que "grep #{params[:keyword]} Gemfile" n'est qu'un sucre syntaxique pour "grep " + params[:keyword] + " Gemfile", mais il n'en est malheureusement rien !
Nous allons donc être forcés d'utiliser la syntaxe send('`', "grep #{params[:keyword].shellescape} Gemfile".shell_safe!) ou créer une autre méthode dans kernel. C'est ce que nous allons faire en aliasant backstick à ` : 
secure_shell.rb
> backstick("grep ".shell_safe! + params[:keyword].shellescape + " Gemfile".shell_safe!) => "..."
Si nous oublions d'échapper l'une des chaine ou d'en désigner explicitement l'une comme étant saine, une exception est soulevée.
> backstick("grep ".shell_safe! + params[:file].shellescape + " Gemfile") ArgumentError: Unsafe command, please use String#shellescape or String#shell_safe!
Refinements
Notre exemple illustre parfaitement les dangers du monkey patching. En modifiant le comportement des méthodes, on s'expose au risque de compromettre d'autres codes qui les utilisent.
Si les autres dépendances de votre application (gems) font des appels de commandes système, et c'est toujours le cas, des erreurs surgiront de partout.
C'est la problématique que les refinements introduits dès Ruby 2 sont venus combler. Découverte.
Les refinements permettent de limiter les modifications des classes à des scopes définis (module, class, bloc begin ; end, etc.).
Pour les utiliser, nous devons englober toutes nos ouvertures de classes dans un module. Nous devons également changer d'approche pour la réouverture de modules. refine ne marche en effet qu'avec des classes : 
secure_shell.rb
La modification des méthodes de Kernel se fait maintenant par la simple inclusion d'un module. Nous n'avons pas besoin d'utiliser prepend ici car nous savons que Kernel est inclut dans Object. Kernel est donc l'un des ancêtres de Objet. Le dernier module inclut dans une classe est placée en tête de liste des ancêtres. SecureShell::KernelMethods passe donc devant Kernel et peut par conséquent utiliser ses méthodes via `super`.
L'utilisation des refinements se fait à l'aide du mot clé using, qui n'est, sans surprise, qu'une méthode de la class Module. Essayons : 
> irb >> $: << '.' >> require 'secure_shell' >> "aString".shell_safe? NoMethodError: undefined method `shell_safe?' for "aString":String >> begin  >>   using SecureShell >>   puts "aString".shell_safe?  >> end  => false >> "aString".shell_safe? NoMethodError: undefined method `shell_safe?' for "aString":String
Nous utilisons irb pour tester notre solution, la première ligne ajoute le dossier en cours au load_path de Ruby, ce qui nous permet de require le code du refinement. Notez comment l'utilisation de la méthode shell.safe? en dehors du bloc begin / end échoue.
Faisons maintenant des appels plus avancés :
> irb >> $: >> require 'secure_shell' >> require 'securerandom' >> begin >>   using SecureShell >> >>   backstick('git init '.shell_safe! + SecureRandom.hex(20).shellescape) >> end NoMethodError: undefined method `shell_safe?' for "git init ":String   from /Users/Ihcene/aritylabs/secure_shell/secure_shell.rb:48:in `+'
Erreur ! Nous constatons que "git init ".shell_safe! fonctionne mais pas l'appel à shell_safe? situé dans notre surcharge de la méthode :+ de String via SecureShell::StringConcat. Il est vrai que ce module est inclus dans String mais il ne bénéficie pas, lui, du refinement dont il fait pourtant partie. Nous venons d'être témoin du caractère très strict par rapport au scope des Refinements, qui fait leur force.
La solution est d'isoler les méthodes #shell_safe!, #shell_safe? et #shellescape dans un refinement imbriqué, qui sera utilisé dans le refinement global lui-même et dans les scopes qui nécessitent de sécuriser les appels de commandes système :
secure_shell.rb
L'ensemble peut être utilisé sur une classe de cette manière :
gist.id
> Gist.new.create => "Initialized empty Git repository in /Users/Ihcene/aritylabs/secure_shell/ecc8984c07440cecfc0badbd02bea560e9c58b1b/.git/\n"
Conclusion
Nous venons de faire la découverte en pratique de la plus importante nouvelle feature de Ruby 2.x. Les refinements nous ont permis de sécuriser l'exécution de commandes système sans prendre le risque d'affecter les autres dépendances.
0 notes
aritylabs · 9 years
Text
Indexer le contenu des fichiers uploadés à votre application Ruby on Rails, avec LibreOffice et Elasticsearch
Permettre l'upload de fichiers fait partie des fonctionnalités qui s'imposent vite à toute sorte d'applications Web. Mais pourquoi s'arrêter là ?
Dans ce tutoriel nous allons explorer la possibilité d'indexer le contenu même de ces fichiers afin de le rendre accessible à l'aide du moteur de recherche interne de votre application.
Tumblr media
   Tout commence avec un modèle Document qui inclut un attachement Paperclip appelé "file"
CODE: document.rb
Nous délèguerons l'extraction en tâche de fond à un processus de l'outil Sidekiq afin de ne pas alourdir l'upload. Mais ne faisons pas directement appel à celui-là, passons par Active Job introduit par Rails 4.1.
Configurons sidekiq d'abord :
CODE: Gemfile > bundle install > touch config/sidekiq.yml sidekiq.yml > bundle exec sidekiq -C config/sidekiq.yml application.rb
Désigner les attachments indexables
document.rb
Chaque fois qu'un modèle déclare un attachment comme indexable via notre méthode de classe `indexable_attachment`, les callbacks nécessaires sont créés. Ces derniers déclenchent le processus d'indexation asynchrone mais vérifient d'abord que le fichier uploadé est d'un type indexable. Via une liste de mime types compatibles que nous placeront dans indexable/supported_types.rb
indexable_attachment est implémentée comme suit :
indexable.rb
Configurons maintenant notre Job. Sidekiq déconseille de passer des enregistrements ActiveRecord aux tâches de fond (pour éviter de mauvaises surprises lors de la désérialisation), mais Active Job fait le nécessaire pour contourner cette problématique grâce à GlobalID::Identification.
L'indexation se fait en quatre étapes via la classe Indexable::FileContent : copier le fichier nécessaire dans un emplacement temporaire, extraire le texte brut via la bibliothèque Docsplit, consolider le résultat puis laisser l'endroit aussi propre qu'en y arrivant !
file_content.rb
Docsplit est une mince couche d'abstraction qui appelle sous le capot différents outils opensource. Dans notre cas, l'extraction du texte brut via la méthode Docsplit.extract_text (ligne 28) fait appel à deux outils : LibreOffice convertit les fichiers en PDF, puis Poppler en extrait le texte brut.
N'oubliez donc pas à installer les différences dépendances de Docsplit.
> (sudo apt-get|brew) install (-y) graphicsmagick poppler-utils poppler-data ghostscript pdftk libreoffice
Du reste, nous utilisons la méthode copy_to_local_file de Paperclip::Attachment et les méthodes usuelles de manipulation de fichiers pour collecter le résultat de l'extraction. D'après le format originel du document, Docsplit peut en effet produire plusieurs fichiers que nous consolidons à l'aide de Dir.glob (et son alias Dir[]).
Où placer ces documents ?
Maintenant que nous arrivons à obtenir le texte brut des fichiers se pose la question de savoir où stocker cette information ?
La question mérite d'être méditée, car il s'agit d'une information volatile recueillie uniquement dans un but d'indexation. Elle peut être retrouvée à tout moment en relançant l'indexation. C'est donc une information à mettre en cache, mais l'extraction du texte brute s'avère lente et couteuse. Nous ne pouvons, dès lors, pas nous contenter du cache Rails classique.
Comme cette information est destinée au moteur de recherche, pourquoi ne pas l'y stocker ?!
ElasticSearch permet de stocker et lire toute sorte de documents structurés. Sa bibliothèque ruby officielle propose des mécanismes de persistances que nous allons utiliser.
CODE: Gemfile > bundle install
Créons maintenant le modèle Indexable::Content qui persistera notre texte brut plus quelques attributes qui permettent de le retrouver.
content.rb
Puis, ajoutons des accesseurs dynamiques via Indexable, pour lire et écrire le contenu depuis Elasticserch.
indexable.rb
Nous pouvons dès lors ajouter cette information à l'indexation du modèle principal
document.rb
Cette solution de persistance est plutôt fiable et il n'y a pas besoin de prévoir un fallback de réindexation. Si pour une raison ou une autre tous les enregistrements persistés sur ES sont perdus, pensez à exécuter une tâche rake qui relance l'indexation de tous vos fichiers.
attachments.rb > bundle exec rake attachments:reindex
Extraire une image d'aperçu du fichier
Tant que nous y sommes, pourquoi ne pas obtenir un aperçu de la première page des fichiers ? À présenter sur les pages de téléchargement ? Pour toute extraction, docsplit convertit le fichier original en PDF en respectant peu ou prou l'emplacement des images et des différents éléments.
Ajoutons à FileContent une étape et à Document la capacité d'enregistrer l'image d'aperçu.
file_content.rb document.rb
Une migration est nécessaire à ce point pour ajouter les colonnes nécessaires à la table documents.
add_file_preview_to_documents.rb
Et si les fichiers sont stockés dans le cloud ?
Si vous avez configuré Paperclip pour stocker les fichiers dans le Cloud d'Amazon (S3) ou autre (inévitable sur Heroku par exemple), le fichier doit être rapatrié afin que le travail d'extraction soit fait. Par chance, ce travail est déjà fait et nous n'avons besoin d'aucune modification à notre code. La méthode copy_to_local_file que nous utilisons est surchargée par l'adaptateur Storage::S3 de paperclip de sorte à télécharger le fichier en question et le placer dans l'emplacement de notre choix.
s3.rb
Déplacer la logique d'indexation vers une application à part
« Le secret pour créer de grosses applications réussies est de ne jamais en créer ». En fonction de la taille et la complexité de votre application, pensez à extraire cette logique d'indexation des fichiers dans un service à part, que vous ferez communiquer avec l'application principale via des appels d'API.
Nous pouvons garder l'interface publique de FileContent comme telle, tout en déportant sa mécanique interne sur une application secondaire, laissant sur place de simples appels d'API.
Conclusion
Pouvoir chercher des fichiers d'après des mots clés qu'ils contiennent peut être une valeur ajoutée d'une extrême utilité pour vos utilisateurs. Pensez à ne pas les en priver !
0 notes
aritylabs · 9 years
Text
Allégez vos modèles en fractionnant la logique, 4 alternatives aux Concerns Rails
Les modèles MVC deviennent très souvent un bazar hétéroclite de méthodes et l’interface publique de ces classes vire vite au chaos désespérant. Tout cela est encouragé par le principe de fat models, skinny controllers qui signifie malheureusement pour beaucoup « ne faites pas de l’orienté objet, vous avez assez de classes avec tous ces modèles qui s’entassent. » 
Nous étudierons dans cet article quatre alternatives différentes et complémentaires aux concerns. À utiliser selon le cas.
Tumblr media
Le problème se ressent le plus dans les modèles centraux d’une application, comme la classe User pour un réseau social, Message pour un board ou Invoice pour une application de facturation. J’ai déjà pu énumérer 290 méthodes dans une seule classe, dont une quantité de one-liners préfixés. Sans compter les accessors par centaines de la table SGBD associée !
L'équipe de développement de Rails recommande l’utilisation de concerns pour isoler et réutiliser des comportements. J’aime bien l’approche et je l’utilise, mais je lui reproche principalement deux faiblesses :
N’encourager l’isolation de logique que pour les comportements communs à plusieurs modèles. Alors que le problème des fat models vient souvent des sous-comportements spécifiques au même modèle.
Injecter des méthodes dans l’interface publique des modèles, qui viennent encombrer toutes les instances créées du modèle. Ce n'est donc qu'une forme d'organisation de votre code en fichiers. Une organisation sympathique pour le développeur, mais sans impact sur l'organisation et l'exécution réelle du code.
Première approche : utiliser de simples nouvelles classes Ruby !
Si vous avez un calcul ou une logique ponctuelle qui s'étend sur plus d'une méthode. Ou pire encore, que vous restreigniez à une seule méthode anormalement longue ou illisible afin de « ne pas polluer » votre modèle, pensez à l'isoler dans une classe Ruby ordinaire. 
Exemple : il vous faut détecter les éventuelles particules nobiliaires des noms de famille des comptes utilisateurs (comme Jean de La Fontaine) : 
Avant ; nous avons deux constantes et trois méthodes autour du lastname. La présence d'un préfixe (ici nobiliary_) est un symptôme intéressant pour détecter les comportements à isoler dans une même classe.
CODE: user.rb
Après ; nous nous sommes débarrassés des préfixes et le tout est concentré dans une classe homogène, facile à tester et à réutiliser.
CODE: user.rb CODE: nobiliary.rb
Si votre logique peut être utilisée par d'autres modèles, placez-là dans le dossier /lib. Si elle est spécifique au modèle comme notre exemple, n'hésitez pas à la placer dans le dossier /app/models/{nom-du-modèle}/. Elle sera "namespacée" avec le nom du modèle et se fera ainsi plus discrète !
Deuxième approche : augmenter le modèle (Facet)
Dans le premier exemple, le couplage entre le modèle et la logique isolée était très faible, seul le nom de famille était en jeux. Si votre modèle intègre par contre une logique qui dépend de plusieurs paramètres (typiquement plus de 4), cette deuxième approche sera plus adaptée.
Prenons pour exemple la fonctionnalité de gamification (ludification) du compte utilisateur. Son code ne sera utilisé que de manière marginale, voire jamais si la gamification est désactivée dans les paramètres de votre application. Pourquoi trainer partout cette logique alors que nous pouvons n'y faire appel que quand on en a besoin ?
Avant
Il s'agit de n’injecter dans l’interface du modèle qu’une seule méthode (gamified), sorte de porte d’entrée vers une version augmentée de notre modèle qui dispose de la logique inhérente au sous-comportement de gamification.
Cette méthode renvoie à chaque appel une instance d’une sous classe qui contient ses propres méthodes et accède allègrement à toutes les méthodes de la classe mère, y compris ses méthodes privées. Cela est rendu possible par l'utilisation de SimpleDelegator comme superclass de notre modèle.
Après :
Nous allons maintenant faciliter la création de ce genre de modules avec la méthode de module ModelOnDiet::Facet :
CODE: gamified.rb CODE: model_on_diet.rb
La méthode Facet (avec une majuscule, car c'est une méthode de module, comme Integer() ou Array(), voir ligne 18) crée dynamiquement un module, utilise le bloc qui lui est passé comme définition de la classe qui sera créée dynamiquement, et qui sera instanciée et "mémoisée" dans une méthode créée dynamiquement. Il ne nous restera donc qu'à inclure ce module dans notre modèle principal pour utiliser.
L'un des nombreux avantages de cette technique est qu'elle permet de faire dépendre plusieurs facets entre eux sans le moindre souci d'injection de dépendances. À partir du code de gamified, nous pouvons par exemple accéder au facet `logged` par le simple appel du nom de la méthode éponyme.
Troisième approche : nuancer le modèle (Context)
La deuxième approche peut servir dans énormément de cas, mais nous l'avons limité aux méthodes d'instances. Si votre modèle a besoin de définir —pour des utilisations occasionnelles— méthodes de classe, callbacks, validations ou scopes, optez cette troisième approche.
Il s'agit cette fois de définir un modèle qui héritera de tout ce dont votre modèle principal dispose tout en lui ajoutant les méthodes et les appels de classe nécessaires. 
Prenons le cas de l'inscription des utilisateurs. Nous avons besoin d'y valider le login et d'envoyer un email avec mot de passe généré aléatoirement.
CODE : user.rb
L'inscription, aussi fondamentale soit-elle pour une application, demeure un cas spécial d'utilisation du modèle User. La logique qui entre en jeux lors de l'inscription est utilisée une fois et ne sera plus utilisée.
Nous définissons alors User::AtSignUp où nous déplacerons cette logique très contextuelle.
CODE : at_sign_up.rb
Seulement, Rails générera des routes erronées et fera des inflections inexistantes. Il faut y définir model_name mais optons pour une solution plus intelligente, que nous pourrons réutiliser plus facilement.
CODE: model_on_diet.rb CODE : at_sign_up.rb
Nous effectuons un héritage indirect via ModelsOnDiet::Context qui définie une classe transitoire où sera définie et d'où sera héritée la bonne méthode self.model_name.
Quatrième approche : maquiller ses instances avec .extend et after_find
Dans un autre article, je présente une solution qui permet de changer l'état de l'instance d'un modèle en injectant à sa classe singleton des modules dont les méthodes ne seront accessibles qu'à cette instance, sans les autres du même modèle.
CODE: user.rb
Enfin, utiliser des concerns sans concerns
Par la présence des dossiers models/concerns et controllers/concerns, Rails recommande ActiveSupport::Concern pour les modules utilisables par plusieurs modèles. Mais n'hésitez pas à l'utiliser pour tout sorte de modules. ActiveSupport::Concern offre plusieurs avantages, dont l'abstraction de l'intégration des méthodes de classe, et l'évaluation de classe (via included), ainsi que la la simplification de l'injection de dépendances.
Conclusion
Nous avons passé en revue quatre approches différentes pour mettre ses gros modèles au régime ! Ces approches et d'autres que nous n'avons pas évoqués peuvent être utilisés dans un contexte de refactoring, mais le développeur consciencieux doit avoir le réflexe de s'interroger en amont sur l'emplacement idéal de ses ajouts de code.
0 notes
aritylabs · 9 years
Text
Email Digest — rationalisez les notifications de vos applications
Jusqu'à ce que votre application devienne un must-have et une dépendance maladive, il faut user de tous les moyens pour y faire retourner les utilisateurs.
Le moyen le plus empirique et le plus accessible reste à nos jours l'envoi de notifications par email. Nous découvrirons dans cet article quelques moyens pour l'exploiter de manière intelligente.
Dans la dernière section de l'article, quelques brides de code Ruby donneront aux développeurs des pistes sur la manière d'implémenter ces idées.
Tumblr media
Quelque chose se passe, n'en parlez pas tout de suite !
Toute amélioration de votre emailing passe par la temporisation de l'envoi de notifications. Laissez à votre application le temps de digérer l'évènement survenu et aux intervenants le temps de rattraper les éventuelles erreurs.
Si un intervenant crée un contenu, l'édite deux fois, le commente et se résout finalement à le supprimer, vos utilisateurs auront reçu 4 ou 5 emails pour un contenu qui n'existe plus !
Parler de plusieurs choses à la fois
La temporisation de l'envoi d'email permet de notifier vos utilisateurs sur différents évènements dans un même email. Si la notification est reportée d'entre 5 et 20 minutes (en fonction de l'activité de votre application), chance que d'autres intervenants aient créées d'autres contenus ou interactions dans cet intervalle.
Vous n'aurez donc qu'a reconstituer le fil des évènements survenu dans cet intervalle
Journaliser les évènements
Pour reconstituer les notifications groupées, il devient important d'enregistrer les évènements survenus dans votre plateforme (créations, modifications, suppressions, commentaires, likes, partages, etc...). La simple présence ou disparition des contenus ne suffira pas à savoir à posteriori ce qui s'est précisément déroulé.
Il convient de prévoir également un moyen de marquer un évènement comme étant annulé : une suppression annule par exemple l'action de création. Un "dislike" annule l'action "like", etc...
Se taire sur ce qu'a déjà vu l'utilisateur, journaliser ce qu'il a vu.
Entre le moment où l'évènement survient et l'instant de l'envoi différé de notification, l'utilisateur peut en avoir pris connaissance. Soit en se connectant à votre application de manière spontanée ou en ayant été appelé par un autre moyen (push, centre de notification, etc...)
Il devient donc important de journaliser les consultations des contenus par l'utilisateur et de les prendre en compte au moment de la construction des emails groupés. Soit en annulant simplement l'envoi ou en n'y parlant que des autres évènement qu'aura raté l'utilisateur.
En profiter pour élaborer des newsletters intelligentes
L'architecture décrite ci-dessus a l'heureuse conséquence de vous permettre d'élaborer avec très peu de surplus de code des rapports périodiques, sortes de newsletters automatiquement formées par l'activité de votre application. Ces rapports seront destinés aux membres moins impliqués mais serviront aussi comme un récapitulatif aux membres fidèles. L'information qu'ils contiennent doit être calibrée parcimonieusement pour ne pas charger les emails et pour servir de teaser
N'hésitez donc pas à les activer par défaut pour tous vos utilisateurs et leur laisser la possibilité d'opt-out.
Simplifier l'accès à votre application à partir des newsletters
Cette logique d'envoi différé et de prise en compte des contenus consultés vous oblige à envoyer un email personnalisé pour chaque utilisateur. Profitez-en pour inclure dans les URL un token d'authentification à usage unique. Il permettra à l'utilisateur de se retrouver directement dans le contenu cible même s'il été déconnecté de votre application. 
Si vous avez des standards de sécurité élevés, vérifiez qu'il s'agit d'un appareil ou d'un ordinateur qui aura déjà servi à une identification directe de l'utilisateur.
Laisser aux utilisateurs le choix de recevoir des notifications instantanées
Il se trouvera toujours parmi vos membres des inconditionnels qui voudront continuer de recevoir des notifications instantanées. Cela peut même être un besoin interne dans la mesure où vos administrateurs/animateurs devront surveiller de près ce qui se passe sur votre application.
L'intégration de l'email digest doit donc conserver le mode d'envoi instantanné-classique. N'hésitez cependant pas à activer l'email digest par défaut.
Talking is cheap, show me the code!
L'implémentation de l'email digest en Ruby on Rails dépend de l'existant, mais voici quelques pistes : 
Nous aimerions conserver les méthodes et les views de nos mailers inchangés et désigner lesquelles sont concernés par la digestion, de cette manière :
CODE : content_mailer.rb
Dans le code qui suit, les appels de la méthode macro `digestable` interpose à chaque nom de méthode passé en paramètre une méthode du même nom. Ces méthodes seront placées dans un module créée dynamiquement et placé en tête du lookup path de notre Mailer (grâce à `prepend`, ligne 16). Les méthodes de ce module passent donc avant les méthodes propre à l'objet, mais reste accessibles dans la hiérarchie de classe.
Les méthodes de remplacement sont elles aussi créées dynamiquement. Leur corps n'est autre que le proc défini à la ligne 24, mais il peut être remplacé par le bloc optionnellement passé à `digestable` (ligne 11). Le premier paramètre de ces méthodes doit être le compte utilisateur (une instance de User) comme nous le validons à la ligne 25. Si la digestion est activée par l'utilisateur et qu'aucun envoi différé n'est déjà prévu (via User#digest_nextsent), l'envoie est enfin programmé. Si digest est désactivé, l'exécusion retombe sur l'envoi classique via `super` (ligne 38).
CODE : mailer.rb CODE : digestable.rb
Le report de l'envoi s'effectue via l'interface Active Job introduite par Rails 4.1 (voir ligne 33). Si vous êtes sur une version plus ancienne, utilisez l'outil de votre choix (sidekiq, delayed_jobs, resque) pour exécuter des tâches de fond. Assurez vous d'avoir des workers fonctionnels et correctement monitoriés.
Nous devons maintenant implémenter DigestMailer et nous voulons que ses méthodes digest et report (pour les rapports périodiques) ressemblent elles-aussi à des méthodes de mailer classique.
S'il se trouve qu'au moment de l'envoi groupé, il n'existe toujours qu'une seule notification, c'est la méthode originale qui est appelée, mais cette fois sans délai (Il n'est pas nécessaire d'opter pour le format groupé pour une seule notification).
Pour l'envoi de rapports périodiques, vous pouvez créé une rake task dont vous programmerez l'exécution chaque matin ou chaque début de semaine. Dans un autre article, je présente en détail un moyen d'exécuter des tâches répétitives avec DelayedJobs.
0 notes
aritylabs · 9 years
Text
Rails : le guide exhaustif de l'application multitenant efficace
L'architecture multitenant ! Un terme à la mode, qu'on retrouve sur toutes les langues dans les SI, mais que signifie-t-il concrètement et comment y parvenir dans notre code ?
Il s'agit dans notre contexte de concevoir une application Ruby on Rails capable de servir plusieurs clients tout en gardant une isolation optimale et sécurisée des données de chacun.
Tumblr media
D'un point de vue applicatif, il s'agit de déployer une seule copie de l'application Rails, qui intercepte les requêtes de plusieurs domaines / sous-domaines ou sous-dossiers et personnalise la réponse en conséquence.  Imaginez le confort en terme de maintenabilité qu'une telle situation vous offre.
Tout au long de cet article, nous allons concevoir une application fonctionnelle et testée qui met en oeuvre l'ensemble de techniques nécessaires.
Le contrat de cette application est le suivant : 
Les données de chaque tenant seront placées dans une base de données qui lui est propre.
Les fichiers de chaque tenant doivent être inaccessibles à partir des autres
Les tenants pourront être identifiés par domaine ou sous-domaine.
La configuration
L'intérêt principal de notre approche est la possibilité de personnaliser indépendamment les tenants. Il nous faut donc un endroit où ces configurations seront stockées et facilement accessibles. 
Parmi ces configurations, nous distinguons les informations propres à la détermination du tenant (domaine associé, nom de la base de données, locale, etc.). Il convient de séparer ces informations du reste de la configuration du tenant. Il existe beaucoup de logique propre au fonctionnement du tenant, ce qui encourage davantage cette séparation, mais nous verrons plus tard son intérêt réel.
Conceptuellement, nous auront alors deux tables : tenants et configurations.
create_tenants.rb
Il est très important de garder à jour votre fichier schema.rb au fil des migrations, car c'est à partir de celui-là que seront créées les bases de données des nouveaux tenants.
Notons que la table tenants sera identifiée par le nom du tenant et non par le `id` classique. Ce nom servira de clé étrangère à la table configurations (ligne 12) et fera partie du nom de la base de donnée, de l'index ElasticSearch et autres identifiants...
Les bases de données
Plusieurs choix techniques s'offrent à vous en fonction de votre système de gestion de base de données. Par exemple, certains SGBD proposent la notion de schemas qui permet d'avoir plusieurs ensembles de tables (et autres types d'objets SQL), dit "schema" dans une même base de données. Tous ces ensembles de données vous seront accessibles via la même connexion. 
Vous avez la liberté d'explorer ces pistes en fonction de vos préférences et vos contraintes. Certains contrats IT exigent l'isolement "physique" des données des clients ce qui fait pencher la balance vers le choix de bases de données séparés, une solution qui peut s'avérer cependant plus couteuse, techniquement parlant.
Pour l'exemple de notre article nous allons opter pour l'approche multi-bases de données, car c'est la plus générique.
Nous utiliserons le gem "apartment" pour faire abstraction des opérations de création et de basculement entre bases de données. Si vous êtes intéressés par l'approche multi-schema, ce gem l'applique par défaut si votre application tourne sous PostgreSQL.
Gemfile
Nous lançons ensuite la commande d'installation de apartment `bundle exec rails generate apartment:install`
Cette commande crée l'initialiseur qui nous permet d'effectuer les configurations de base dont l'exclusion des modèles Tenant et Configuration afin que leurs données résident dans la base principale de l'application : 
config/initializers/apartment.rb
Les tables des modèles exclues seront toutefois créées sur les bases de tous les tenants, car le schema.rb qui permettra de déployer votre application la première fois est le même qui servira à la création des bases des tenants.
Apartment propose plusieurs "ascenseurs" (elevators) pour basculer automatiquement de contexte. Il en existe pour les domaines, les sous-domaines, des hashes de domaines... 
L'ascenseur des domaines peut être un bon compromis, car les clients finissent toujours par vouloir disposer de leur propre nom de domaine, surtout quand ils passent en "premium" ou en marque-blanche. Le subdomain elevator est donc à éviter.
Nous allons opter pour le plus générique des élévateurs et nous déportons la logique vers la classe Tenant.
apartment.rb
Dans la ligne 10, le middleware attend que la classe Tenant retourne un proc via to_proc. Ce que nous allons implémenter dans le module `requestable` (ligne 6).
tenant.rb requestable.rb
Nous sommes arrivés au point où il faut changer de contexte en invoquant Apartment et les mécanismes connexes. Faisons cela dans un autre module afin de départager les responsabilités :
tenant.rb contextable.rb
Cool, mais nous mettons ici les charrues avant les bœufs. La création de tenants doit d'abord être reliée à la création de bases de données. Assurons également à chaque tenant sa configuration.
tenant.rb configurable.rb creation.rb
Achevons de peaufiner notre système en prévoyant un préfixe pour les noms des bases de données. Si votre SGBD risque d'héberger des bases d'autres applications, cela réduira les risques de conflit de nommage.
tenant.rb prefixable.rb
Avant de passer à d'autres aspects, remarquons que les appels à `Tenant.current` (qui risquent d'être nombreux dans notre application) nécessitent à chaque fois des requêtes en base de données.
Nous allons donc mettre l'instance du tenant et sa configuration en cache :
tenant.rb cachable.rb
Le cache est ici double. D'un côté, nous stockons le temps d'une requête le nom du tenant dans le "thread" courant. Cet identifiant est utilisé comme clé pour lire dans le cache. L'instance de Tenant et sa configuration sont supprimées du cache dès que l'instance est sauvegardée ou "touchée" (.touch). Pour cela, une petite modification du modèle Configuration s'impose :
configuration.rb
Les migrations
Surchargé par Apartment, rake db:migrate migre toutes les bases de données des tenants figurant dans la configuration config.tenant_names de Apartment. Il migrate également (et surtout) la base de données principale.
Servons à ces tâches la liste des noms de bases de données préfixées correspondant à chaque tenant :
apartment.rb requestable.rb prefixable.rb
Seulement, ce modèle n'est pas compatible avec les migrations qui manipulent des données. En effet, les migrations s'exécutent sans middlewares ni requêtes et Apartment change de base de données sans changer le contexte ActiveRecord.
Nous allons donc prévoir un mécanisme pour changer le contexte en fonction du nom de la base de données. De ce nom, nous remontons vers le tenant correspondant et nous changeons vers son contexte.
D'ailleurs, profitons-en pour coder quelques méthodes qui nous seront utiles dans le code et la maintenance de l'application.
"run" permet de changer de contexte, exécuter un bloc de code et revenir au contexte précédent. Ça peut être utile si vous devez faire une lecture de données entre un tenant ordinaire et un tenant "spécial".
CODE : contextable.rb
`current` renvoie l'instance du tenant en cours, qui permet d'accéder à la configuration associée comme :  > Tenant.current.config.name.
Dans l'utilisation de tous les jours dans votre code, il convient de raccourcir cette syntaxe.
CODE : application_controller.rb
Nous définissons ici deux méthodes, config et son raccourci `c`, toutes deux accessibles dans tous les contrôleurs (par héritage) et dans toutes les vues également (via helper_method) 
Les sessions
Aucune intervention n'est nécessaire pour sécuriser les sessions. Rails limite par défaut les cookies et spécialement le cookie de session à un seul domaine. Si un même utilisateur est identifié sur un sous domaine de votre application multitenant puis accède pour la première fois à un autre sous-domaine, une nouvelle session vierge sera créée et les deux sessions n'interagiront pas.
De même que si vous persistez les sessions dans la base de données, chaque tenant aura sa propre table "sessions" et la lecture de ces données s'effectue après le changement de contexte du tenant.
Si vous héritez d'une application existante, assurez-vous que le paramètre domain: :all n'est pas défini dans l'initialiser session_store.rb ou ailleurs dans application.rb ou environments/production.rb.
CODE : session_store.rb
Gérer les tenants introuvables
L'utilisation du middleware suffit pour changer de contexte à chaque requête vers votre application. Mais que faire des requêtes dirigées vers des tenants inexistants ?
CODE : application_controller.rb
Dans l'environnement de production, nous redirigeons vers le site corp, celui qui vous permet d'inscrire vos clients. Vous pouvez pousser le bouchon plus loin en proposant à l'utilisateur de créer son propre tenant, dans le cas où votre business-model implique une création ouverte des tenants.
Nous utilisons les `custom configurations` de Rails 4, que nous placerons dans un initialiser pour ne pas polluer application.rb
CODE : config.rb
Les fichiers
Au même titre que les données en base, le cloisonnement des fichiers est important pour toute application multitenant.
Nous allons prendre en exemple l'utilisation du gem populaire paperclip.
Nous modifierons l'emplacement où les fichiers attachés seront stockés, en les incorporant dans un dossier qui correspond au nom du tenant.
Mais cela n'est pas suffisant, car les fichiers de tous les tenants peuvent être accédés via les domaines des autres tenants. Nous n'en pouvons pas grand-chose car tous les serveurs Web délivrent les contenus statiques en priorité par rapport aux contenus dynamiques. Inutile donc de chercher à occulter les fichiers des autres tenants via des routes catch-all.
Des solutions plutôt hardcore peuvent être envisagées, comme de ne pas stocker des fichiers dans le dossier public et les délivrer via send_file à partir d'un contrôleur dédié. Une autre solution, encore plus hardcore, peut être discutée, à savoir éditer —au moment de la création du tenant— le fichier de configuration du serveur Web pour ajouter des règles qui relient les domaines avec les dossiers.
Pour les besoins de cet article, nous nous contenteront d'une solution qui, au lieu de rendre les fichiers inaccessibles, brouille les pistes pour les trouver. Nous remplaceront l'id par un hash sécurisé et nous englobantsles fichiers d'un tenant dans un dossier commun afin de faciliter leur sauvegarde et exportation.
CODE : paperclip.rb
Les emails
L'envoi de courriels via ActionMailer implique d'éviter toute personnalisation au niveau de la déclaration de classe de vos Mailer.
Les principaux paramètres de la méthode "default" peuvent prendre soit des valeurs littérales ou des procs exécutables. On peut d'alors faire :
CODE : app/models/concerns/multitenant_mailer.rb CODE : app/models/main_mailer.rb
Mais il reste le problème épineux des default_url_options. Ce paramètre ne prend pas de valeur exécutable. Nous devons donc le (re) définir au moment du changement de contexte, au sein même de la classe Tenant.
CODE : app/models/tenant.rb CODE : app/models/tenant/mailing.rb
Tâches de fond (delayed_jobs)
Il existe un gem dédié à l'intégration de apartment avec sidekiq, le système de gestion de tâches de fond qui monte. Delayed Jobs bénéficiait d'une intégration approximative qui a été retirée depuis par faute de stabilité.
Le problème, vous l'auriez deviné, vient encore de l'absence de changement de contexte. Les workers delayed jobs se lancent en rake task et traitent les tâches de fond sans traverser la chaine des middlewares.
Pire encore, Delayed Job fonctionne de manière à charger automatiquement de la base de données les paramètres passés à la méthode delayée et serialisés. Ce chargement se fait hélas dans le contexte de la base de données principale de l'application.
Avant toute chose, ajoutons Delayed::Job aux modèles exclues du multitenant. Les tâches de tous les tenants seront toutes stockées dans la même base.
Gemfile CODE : config/initializers/apartment.rb
Ensuite, ajoutons une nouvelle colonne à la table delayed_jobs, qui véhiculera le nom du tenant dans le contexte duquel les tâches devront être exécutées.
CODE : db/migrations/
Place aux hacks maintenant. Assurons-nous de renseigner cette colonne à chaque fois qu'un job est créé. Nous devons le faire dans plusieurs endroits suivant les différentes manières qu'offre delayed_job pour rendre asynchrones les tâches.
La syntaxe : object.delay.some_method(arg...)
Nous surchargeons la méthode delay via la génialissime killer-feature de Ruby 2, prepend. Cette dernière nous évite de faire du sale monkey patcking via `alias_method_chain` ou renommage manuel de methods.
CODE : apartment.rb
`Prepend` mérite un article à part, mais, disons rapidement qu'elle permet d'insérer un module au tout début de la chaine des "ancestors", qui est traversée à chaque appel de fonction (message envoyé) pour trouver un "répondant" au message. Pour une classe, cette chaine commence par elle même ! Prepend insère votre module devant même cette classe. Ce qui a deux heureuses conséquences :
Une méthode de votre module est prioritaire à celle du même nom sur la classe originale.
Vous pouvez appeler la méthode de la classe originale depuis votre méthode du même nom, avec “super”
La syntaxe : Delayed::Job.enqueue(job_instance)
Nous utilisons la même technique, à la différence près que nous prependons notre module dans la classe singleton de Delayed::Job, afin de surcharger ses méthodes de classe :
CODE : apartment.rb
Nous passons à présent au changement de contexte, qui dois survenir à la première évocation de la méthode Delayed::Job#payload_object
CODE : apartment.rb
L'envoi d'emails en tâche de fond
Si votre application gère l'internationalisation, vous faites typiquement le changement de locale dans un before_filter de application_controller. Mais ce dernier ne s'exécute pas si vos emails sont envoyés d'une tâche de fond. C'est pourquoi nous avons prévu dès le début d'article le changement de locale au moment du changement de contexte (dans Tenant#switch)
Notons également que l'envoi d'email se fait avec Delayed Jobs via la classe ActiveMailer::Base. C'est donc sur celle-là que nous allons intervenir :
CODE : apartment.rb
Moteur de recherche avec ElasticSearch
Si vous utilisez ElasticSearch pour la persistance des données, nous vous recommandons d'envisager d'autres pistes de réflexion pour mieux cloisonner vos données.
Si par contre vous ne l'utilisez que pour indexer / trouver des contenus persistant en base déjà cloisonnées, la moins couteuse des solutions est d'utiliser l'index name pour séparer vos tenants. Avec la gem officielle `elasticsearch-model`, cela peut se faire très facilement.
Gemfile searchable.rb
Et c'est tout, il vous suffit ensuite d'inclure ce module dans tous vos modèles indexables et vos données iront automatiquement au nom index.
Mise en cache
Le cache étant également une information volatile, nous utiliserons le même mécanisme et le même "récipient" pour tous les tenants. Rails propose un paramètre namespace très pratique.
CODE : application.rb
Et c'est (à peu près) tout ce qu'il faut. Si pour une raison ou une autre vous devez vider tout le cache d'un tenant (pour un changement de langue par exemple), sachez que Rails.cache.clear est insensible au paramètre namespace. Il videra le cache pour tous les tenants, ce qui peut compromettre les performances globales de votre application. 
Nous utiliserons plutôt Rails.cache.delete_matched(/.+/) qui fait du cas-par-cas.
cachable.rb
Ce code nous permet d'appeler Tenant.current.cache.clear, handy!
Notez cependant que cette méthode ne fonctionne que pour les cache stores qui supportent les expressions régulières sur la liste globale des entrées existantes.
Multi-serveurs / multi instances
Jusque-là, nous sommes arrivés à concevoir une application qui sert plusieurs clients et qui isole les données de chacun de manière satisfaisante.
Mais si votre business évolue et votre serveur atteint ses limites, vous devriez en prendre d'autres et la solution actuelle restreindra votre évolution.
Nous verrons dans un article prochain comment faire évoluer votre application multi-tenant en une véritable application SaaS scalable et multi-serveurs.
0 notes
aritylabs · 9 years
Text
Champs virtuels avec ActiveRecord/Model : typés, validés, avec valeurs par défaut !
Le besoin en colonnes et tables virtuelles s'impose vite à mesure que votre application augmente en complexité. Il arrivera certainement un moment où vous voudrier arrêter d'ajouter via migrations maintes et maintes colonnes, que ce soit par économie conceptuelle ou par besoin : votre application peut avoir besoin d'un nombre variable de colonnes.
Dans cet article, nous allons décortiquer les moyens d'y arriver, en explorant les composants de Active Record et Active Model qui entrent en jeux.
Tumblr media
TL;DR l'ensemble du code de cet article a été compilé en une Rubygem disponible sur Github.
Serialize
Avec un peu de chance, cette méthode macro de ActiveRecord ne vous aura pas échappé :
client.rb
Vous n'êtres probablement pas sans savoir qu'elle prend un deuxième paramètre qui détermine le seul type de donnée qui peut être sérialisé. Sans quoi, tout ce que vous passez à contact sera sérialisé en règle.
client.rb
En deuxième paramètre, elle peut prendre n'importe quelle classe pour peu que vous passez à instance.contact= des instances de cette même classe.
client.rb
Dans vos formulaires, vous pouvez utiliser .fields_for comme vous l'utiliseriez pour une association
new.html.erb
Cette méthode rend énormément service, mais a l'inconvénient d'être malgré tout peu structurée. Que vous lui passiez un Hash ou un Struct ou une classe toute faite avec accesseurs, ses données ne bénéficieront ni de validation ni de cast (forçage de typage), ni de valeurs par défauts.
Nous allons donc élaborer une solution qui répond à ces besoins tout en reposant sur les mêmes mécanismes utilisés par ActiveRecord.
client.rb
Surcharger serialize
La première chose à faire est de permettre à serialize de prendre un bloc comme paramètre. Nous faisons cela en mettant une nouvelle méthode serialize devant celle qui se trouve dans ActiveRecord::AttributeMethods::Serialization::ClassMethods.
CODE : VirtualAttributes::Serialization::ClassMethods CODE : VirtualAttributes
La nouvelle méthode serialize prend un bloc optionnel. Si ce bloc est absent, c'est la méthode serialize "classique" qui est appelée, autrement nous déclenchons la procédure de construction de notre sous-modèle.
Construite notre sous-modèle 
Nous avons donc un block que nous pouvons maintenant utiliser pour construire notre sous-modèle. Ce dernier est une classe qui sera dynamiquement créée et placée sous le namespace du model principal. 
CODE : VirtualAttributes::Serialization v2 CODE : VirtualAttributes::Base CODE : client.rb
La classe hérite de VirtualAttributes::Base qui contiendra la logique nécessaire pour créer les attributs. const_set est ensuite utilisée pour placer cette classe dans le namespace adéquat. Yield permet d'appeler des méthodes de classe pour ajouter les colonnes qu'on veut. Nous faisons enfin un appel à la version originale de `serialize` (via super) en spécifiant cette fois notre nouvelle classe comme type de sérialisation.
L'ajout des attributs se fera via la méthode de classe `column` que nous allons implémenter dans un module séparé :
CODE : VirtualAttributes::Base::Attributes CODE : VirtualAttributes::Base avec un simple `include`
Nous pouvons déjà ajouter des colonnes mais elles ne seront pas typées :
CODE : client.rb
Place aux types maintenant :
Ajouter des attributs typés
Nous allons tenter d'implémenter une interface similaire à celle des migrations de Ruby on Rails. Pour cela, il nous faut une méthode pour chaque type disponible : nous allons utiliser la liste des types disponibles par le SGBD en cours d'utilisation, exception faite du type :primary_key pour des raisons évidentes.
CODE : VirtualAttributes::Base::Casts CODE : VirtualAttributes::Base avec un simple `include`
Toujours dans une approche modulaire, VirtualAttributes::Base::Casts surchargera et utilisera certaines méthodes de module Attributes. Il s'agit de la méthode de classe `column` et de la méthode d'instance `write_attribute`. 
Lorsque le module Casts est inclu, il modifie la liste des arguments de la méthode column de Attributes (en ajoutant le paramètre type) mais continuer d'utiliser cette "version" qui ne prend que deux paramètres. C'est une sorte de limitation de responsabilité verticale, qui traverse la chaine.
La nouvelle version de write_attribute fait la même chose, sauf qu'elle ne modifie pas la liste des paramètres, elle altère la valeur de l'un d'entre eux (!), pour effectuer la conversion de type (type cast).
Cette conversion se fait via le flambant neuf module ActiveRecord::Type, qui a profondément remodelé la conversion de type sur Rails 4.2. Ce module est utilisé par les différents adaptateurs de SGBD (mysql, postgres, sqlite, etc...) afin de gommer les différences qui existent entre leurs types natives de données.
Dans notre cas, puisque les données seront sérialisées et stockées dans un champ texte tout bête, nous n'avons pas besoin d'utiliser un adaptateur spécifique. Les classes génériques présentes dans ce module suffiront.
En fonction du type de la colonne, notre code instancie la bonne classe (ActiveRecord::Type::Float, Decimal, String, Binary...) et la met à la disposition de la méthode cast_type.
Nous créons par la suite une méthode de classe correspondant à chaque type. Ces méthodes délèguent toutes à `column` en lui passant le type adéquat (lignes 27-35).
Utiliser des valeurs par défaut
Passer des valeurs par défaut était déjà prévue dans le paramètre "options" de column.
Le module Defaults surcharge read_attribute pour intervenir dans le cas où read_attribute du module Attributes, appelé avec `super` retourne `nil`.
CODE : VirtualAttributes::Base::Defaults CODE : VirtualAttributes::Base avec un simple `include`
Toujours suivant les méthodes des migrations, nous ajoutons des valeurs par défaut. Et ça marche !
CODE : client.rb
Des valeurs par défaut exécutables
Vous avez probablement déjà fait face à une situation qui nécessite une valeur par défaut dynamique. Le constat est vite fait conclu que ce n'est pas faisable via la valeur par défaut des migrations. Une solution est de n'attribuer aucune valeur par défaut à votre champ et contourner cela dans after_initialize.
Mais nous ne sommes pas confrontés à cette limitation ici. La valeur par défaut est stockée en mémoire, dans l'attribut de classe `columns`.
Autorisons alors que ce paramètre soit un bloc exécutable : 
CODE : VirtualAttributes::Base::Defaults CODE : client.rb Et enfin, les validations !
Nous continuons sur la lancée modulaire en ajoutant le module Validations. Pourtant, celui là ne fait qu'inclure le module ActiveModel::Validations. 
CODE : VirtualAttributes::Base::Validations CODE : VirtualAttributes::Base avec un simple `include`
Sa présence est tout de même importante dans ce qui suit. Nous devons ajouter une validation à chaque champ sérialisé avec notre solution.
CODE : VirtualAttributes::Serialization avec validates attr_name, virtual_attributes: true et VirtualAttributesValidator
La validation se fait via un custom validator de type EachValidator. La logique de validation ne s'exécute que si la classe utilisée inclut le module VirtualAttributes::Base::Validations.
L'ensemble des erreurs éventuelles présentes dans les instances de la classe dynamique sera regroupée dans une même erreur.
Utiliser les champs virtuels dans les formulaires
Les champs peuvent être utilisés dans vos views à l'aide de `.fields_for`. Ainsi, de manière identique aux formulaires classiques
CODE : new.html.erb
Mais au moment de l'envoi du formulaire, le champ serialisé reçoit une instance de ActionController::Parameters et non une descendante de VirtualAttributes::Base. Cette instance sera rejetée, car le type a été strictement spécifié au moment de la génération.
Nous devons intervenir à cet endroit précis pour convertir le paramètre reçu en une instance de la bonne classe !
CODE VirtualAttributes::Serialization
Nous avons défini le attr_writer de notre champ. Mais avant de le passer à super, nous appelons Wrap du nouveau module Conversions
CODE VirtualAttributes::Base::Conversions CODE VirtualAttributes::Base include conversions
Si la valeur reçue est une instance de Hash, nous l'utiliserons pour créer une nouvelle instance du bon type : 
CODE VirtualAttributes::Base::Attributes (initialize)
Bingo !
Et le tour est joué. Les formulaires de création et d'édition gèrent très bien la lecture et l'écriture dans nos champs virtuels, qui bénéficient de la conversion de type et de la validation tout en supportant des valeurs par défaut statiques et dynamiques.
Conclusion
Nous avons passé en revue l'ensemble des composants qui peuvent entrer en jeux pour élaborer des solutions robustes et extensibles d'ajouts de champs virtuels à vos tables.
Dans un prochain article, nous verrons comment rendre ces champs trouvables en combinant les méandres de ActiveRecord à ElasticSearch.
0 notes
aritylabs · 9 years
Text
Good practice: bannir `send` pour les messages dynamiques
Quand je passe en revue du code Ruby, je tombe souvent sur des utilisations de send à éviter. C'est une méthode présente sur tous les objets, permettant d'envoyer un message (aka. appeler une méthode) en passant en paramètre le nom de la méthode voulue.
Tumblr media
Cet usage est fautif et à bannir si on y a recourt pour effectuer des appels dynamiques : envoyer un message sans savoir jusqu'au dernier moment lequel (nom reçu en paramètre, itération sur plusieurs noms de méthodes, etc...). 
Pour cause, l'autre usage notoire de send n'est autre que de violer à dessein l'interface publique des objets, pour appeler des méthodes privées ou protégées.
Exemple pour inclure un module dans un autre sans réouvrir celui-là (ìnclude` est une méthode de classe privée) :
ActiveRecord::Base.send :include, MyFancyModule
L'alternative est d'utiliser public_send, qui n'invoke, comme son nom l'indique, que les méthodes publiques de vos objects.
Vous serez ainsi certain de ne pas appeler par mégarde une méthode privée qui peut disparaitre ou changer d'interface à tout moment. Rappelons qu'en POO, seule l'interface publique d'une classe est censée être conservée.
Cheers
0 notes
aritylabs · 9 years
Text
Détourner DelayedJobs pour exécuter des tâches répétitives
À mesure que votre application se complexifie, le besoin d'exécuter des tâches répétitives finit toujours par se faire sentir. C'est généralement des besoins fonctionnels qui exigent ce genre de tâches (envoi de rapports périodiques, de rappels d'expiration prochaine, etc.) mais c'est souvent des soucis de performances qui mènent à effectuer des calculs et des traitements par lots périodiques.
En Ruby, DelayedJob est l'outil le plus populaire pour exécuter des tâches de fond. Nous verrons dans cet article comment l'exploiter au mieux pour programmer l'exécution de tâches répétitives.
Tumblr media
Quelles alternatives ?
Rails ne propose aucun mécanisme pour exécuter du code à un instant t. Il est vrai que le code de l'application est chargé en mémoire, mais ce code demeure dormant jusqu'à ce que l'instance de votre application soit invoquée ou forkée par l'arrivée d'une requête HTTP.
Le plus simple moyen d'exécuter du code de façon périodique est de créer une tâche rake et programmer l'exécution de celle-là via le crontab du système, mais cette approche a plusieurs inconvénients :
Prévilèges système : devoir avoir accès à l'édition de crontab, qui passe par une session ssh, voir des droits root selon la configuration de votre intégration Rails avec le serveur Web.
Lenteur : l'exécution d'une rake task requiert généralement le rechargement quasi complet du code de votre application et de ses dépendances (gems). Si votre application contient beaucoup de code, ce temps de chargement peut atteindre des dizaines de secondes. Compromettant si votre application doit s'exécuter à des intervalles rapprochés.
Régidité : le timing d'exécution des tâches est écrit en dur dans le fichier crontab et ne peut donc pas être changé dynamiquement sur votre application. 
Dispersion de l'information : le timing d'exécution de votre code est une information qui sort de votre application. Elle n'est pas versionnée et peut se perdre.
Pourquoi DelayedJob ?
Delayed Job fonctionne en faisant tourner des workers qui vérifient continuellement dans la table delayed_jobs l'existence de tâches en attente et dont la date d'exécution est arrivée.
C'est cette mécanique bien huilée que nous exploiterons.
Réalisation
Le modèle central de delayed_job est Delayed::Job. Créons à notre tour Delayed::Cron dont hériteront tous les jobs qui devront être exécutés périodiquement. De cette manière : 
CODE 1
CODE 2
Rien de vraiment spécial, si ce n'est que FunnyJob utilise comme méthode macro la méthode de classe every qu'elle hérite de Delayed::Cron.
Maintenant, enregistrons ces paramètres dans une classe dédiée et élaborons la logique qui permet de calculer le prochain délai d'exécution de la tâche : 
CODE 3
params_list.rb
next_term.rb
La classe ParamsList enregistre le paramétrage du cron et le complète avec des paramètres par défaut (ligne 23). NextTerm calcule quant à elle le prochain délai d'exécution en devinant la date la plus proche et en s'assurant via la méthode futur! (ligne 17) que cette date n'est pas passée.
Planifions tout cela !
L'idée est de s'assurer que chaque cron planifie la prochaine exécution, même en cas d'échec (voir le ensure lignes 7 et 8)
cron.rb
Delayed Jobs exécute la méthode perform des jobs passés à Delayed::Job.enqueue. Le code qui utilise les crons doit concentrer sa logique dans la méthode perform_cron qui sera à son tour appelée par perform.
À la planification, nous nous assurons qu'aucune tâche planifiée n'a été dédoublée. Si c'est le cas, clean_up se charge de supprimer les improbables doublons.
Renforcer la sécurité en chargeant les crons au démarrage
S'il arrive un pépin au moment de l'exécution du bloc ensure ou si l'échec de l'exécution du job fait crasher tout le worker, la chaine est brisée et une intervention manuelle sera nécessaire pour la rétablir. C'est pourquoi nous exécuterons la méthode schedule de tous les jobs au chargement de l'environnement de l'application, qui survient également au démarrage des workers et autres consoles ou rake tasks.
cron.rb
Nous avons ajouté deux méthodes de classe à Delayed::Cron. Nous pouvons passer à load une liste de classes à surveiller au démarrage de l'environnement.
Ou appeler load_all qui charge toutes les classes présentes dans le dossier app/crons. Ce dossier peut être personnalisé dans l'unique paramètre que prend cette méthode. Load_all ajoute ce dossier aux chemins d'autoload de l'application (ligne 46).
Notons à la ligne 40 l'utilisation de logger.silence qui empêche l'application de journaliser tout cela. Le démarrage de l'application en mode console (rails c) ne sera donc pas pollué.
Ces appels se font idéalement dans un initializer (exemple : config/initializers/delayed_crons.rb
delayed_crons.rb
Conserver les crons qui échouent sans les rééxécuter
Delayed Jobs permet de conserver les jobs qui fail mais ne donne pas la possibilité de choisir lesquels. Ils sont soit tous retenus soit tous supprimés suivant le paramètre global Delayed::Worker.destroy_failed_jobs = true|false.
Nous devons donc ruser pour conserver, pour des raisons de débogage, les crons qui échouent, mais nous ne pouvons pas leur permettre de s'exécuter d'autres fois. Leurs échéances auront passé et ils risquent de perturber ou dédoubler les prochaines exécutions.
config.rb
config.rb
Nous permettons (en théorie) au cron de s'exécuter deux fois, sauf que la deuxième exécution n'aura pas lieu de sitôt ! Si quelqu'un trouve une idée plus propre qu'il nous en fait part.
Et pourquoi pas la syntaxe `cron` ?
Nous ne pouvons évidemment parler de crons sans leur célèbre syntaxe étoilée (!) : mm hh jj MMM JJJ (minutes, heures, jour du mois, mois et jour de semaine). Essayons de rendre possible une telle utilisation :
config.rb
Dans cette implémentation, la classe CronFormat parse la fameuse chaine étoilée et devine la fréquence adéquate et les paramètres nécessaires. :
cron_format.rb
params_list.rb
Worker dédié
Si vous en avez la possibilité, reservez au moins un worker DelayedJob à la queue `delayed_crons`.
Le worker dédié pourra être lancé de cette manière, à partir de vos tâches Capistrano, vos pillules bluepill ou autre !
delayed_job -i 1 --queue=delayed_crons start
Conclusion
Dans cet article, nous avons étudié l'approche globale de l'exploitation de DelayedJobs pour effectuer des tâches répétitives. Nous planifions la prochaine exécution d'une tâche à chaque exécution et — en backup — à chaque lancement de l'environnement.
L'implémentation est toutefois très optimiste quant aux paramètres passés à every. Charge à vous de la peaufiner en ajoutant les validations nécessaires ! ;)
0 notes
aritylabs · 9 years
Text
Maquillez vos modèles avec after_find
Les développeurs sont souvent confrontés aux “cas spéciaux” qui peuvent, s’ils sont mal gérés, ruiner leur architecture et polluer leur code.
Nous verrons dans cet article un moyen qui permet de “maquiller” ses modèles Ruby on Rails pour les cas spéciaux, avec un callback quelque peu méconnu d’ActiveModel.
Tumblr media
Prenons le cas d’un réseau social où les utilisateurs peuvent s’inscrire, collaborer et… se faire éventuellement bannir.
Nous aimerions que les utilisateurs bannis disparaissent du réseau, mais que leurs contributions restent anonymement visibles. Le nom de l’utilisateur devient “Utilisateur banni”, son avatar une image par défaut, sa fiche profile vierge, sa liste d’amis vide…
Il va sans dire qu’éparpiller ce comportement sur les différentes views serait une horreur. C’est donc logiquement au sein du compte utilisateurs que les modifications seront apportées :
CODE
Nous voyons tout de suite ce qui ne va pas. Le cas spécial (et rare) de bannissement s’impose comme une partie intégrante de la logique du modèle. On peut tout au mieux isoler ces redéfinitions de méthodes dans un concern mais cela ne fera que cacher le problème : ces tests s’exécuteront pour tous les comptes, qu’ils soient bannis ou pas.
Autre problème à noter : nous écrasons de cette manière les méthodes pour y inclure notre test. Quand le modèle de classe offre une alternative (comme read_attribute) tout va bien, mais si ce n’est pas le cas comme pour l’avatar, nous sommes obligés d’aliaser la méthode pour la réutiliser dans le cas où l’utilisateur n’est pas banni.
Et puis, que faire si nous devons gérer d’autres cas spéciaux, comme les comptes suspendus, expirés… ? Imbriquer les tests ?
after_find au secours
ActiveRecord propose un callback quelque peu méconnu, qui s’enclenche chaque fois un enregistrement trouvé et chargé. C’est after_find. Nous pouvons en profiter pour maquiller uniquement les comptes bannis.
Isolons d’abord la logique spéciale aux comptes bannis et enlevons-lui les tests inutiles :
CODE
Et intégrons-là aux seules instances des comptes bannis.
CODE
Aussi simple que cela !
Un mot sur extend
Nous appelons extend sur les instances de User dont l’attribut banned? renvoie true. extend ajoute les méthodes du module en paramètres aux méthodes d’instance de l’objet.
Une classe est une instance de la class… Class, les méthodes de cette instance sont donc les méthodes de class de la class ! C’est pourquoi appeler extend dans la définition d’une class ajoute les méthodes du module aux méthodes de class.
En réalité, extend n’ajoute pas les méthodes du module aux méthodes d’instance d’un objet. Elle inclut ces modules dans la class singleton de l’objet.
Vérifions :
> User.banned.first.singleton_class.ancestors => [User::Banned, User(id: integer,...]
Conclusion
after_find peut être un moyen très efficace pour changer élégamment l’état de vos objets. À utiliser sans modération.
0 notes
aritylabs · 9 years
Text
Définir des callbacks au runtime — Rails
Les callbacks ActiveModel sont géniaux, mais souffrent de la rigidité d’être dictés par la définition du model et de s’appliquer ainsi à l’ensemble des instances qui en découlent.
Certes, ils permettent de définir des prédicats :if ou :unless qui aident à déterminer au moment de l’exécution si tel et tel callback doit être exécuté, mais ces callbacks sont quand même là, à rallonger et alourdir la chaine d’exécution.
Tumblr media
Dans bien des cas, le besoin de déclarer des callbacks au niveau de l’instance se fait sentir. Prenons l’exemple d’une application qui interagit avec un webservice pour l’un de ses modules. Un appel d’API à ce service doit être lancé pour inscrire ou désinscrire l’application quand le module est activé / désactivé
Pour notre exemple, l’action se déroule autour du model Configuration qui contient les réglages de chacun des clients qui utilisent votre application multitenant.
Vous mettez à disposition de chaque client un formulaire qui permet de sélectionner les modules activés, dont celui qui nécessite un appel d’API.
Vous pouvez accomplir cela de plusieurs manières, plus ou moins perfectibles
Tout faire dans le contrôleur
CODE 1
Inutile de rappeler que toute logique ajoutée au contrôleur devrait être vue d’un mauvais œil. D’autant qu’il s’agit là d’une action spécifique au modèle que nous interceptons au moment du changement de ce dernier.
Utiliser un callback au niveau du modèle
CODE 1
CODE 1
Ça va tout de suite mieux, l’activation/désactivation du module est interceptée après l’enregistrement. Notons au passage le “true” à la fin du callback. Comme les méthodes subscribe/unsubscribe sont susceptibles de renvoyer false au cas où l’appel d’API échoue, il devient nécessaire de spécifier la valeur de retour de la méthode. Rappelons que le moindre false comme valeur de retour d’un callback halte toute la chaine d’exécution et foire silencieusement l’action qui l’a déclenche. On ne le rappellera jamais assez !
Faire mieux ?
Mais comme expliqué dans l’introduction, cela encombre la chaine des callbacks et pollue la déclaration du modèle avec des appels de macros pourtant très contextuels.
Une solution est d’intercepter l’activation/désactivation au moment même de l’écriture de cette information, de cette manière :
CODE 1
C’est plus satisfaisant, la logique se fait au niveau du writter accessor et ce code ne s’exécute que lorsqu’une valeur est effectivement passée. Si vous avez un autre formulaire dédié qu’aux informations du client sans les modules, ce code ne s’exécutera pas. Mais il a un défaut très handicapant : l’appel d’API se fait sans être certain que l’enregistrement se fera. Si l’action échoue à la validation ou à un autre callback qui renvoi false, on aura inscrit la plateforme alors que le module reste désactivé !
Cette méthode peut pourtant servir, dans l’état, sans callbacks, pour exécuter du code SQL réversible. update_attributes (qui repose sur .save) s’exécute et exécute tous les callbacks dans une transaction. Si le moindre pépin arrive, un rollback est effectué.
Des callbacks à l’exécution
Alors, pour les actions irréversibles, comme l’appel d’une API externe ou l’envoi des emails, sommes-nous condamnés à suivre la piste des callbacks classiques ?
Oui à moins de créer une alternative qui nous permettrait de faire ceci :
CODE 1
Pour y arriver, essayons de comprendre le fonctionnement des callbacks. Qui ne relève d’aucune magie.
Comprendre les callbacks
Les callbacks tels qu’on les connaît sont un module de ActiveModel extrait historiquement de ActiveRecord. Ils peuvent être utilisés par n’importe quelle class pour exécuter du code avant, après ou autour des appels de certaines méthodes.
Leur utilisation commence (et se termine) par la simple inclusion du module ActiveModel::Callbacks dans vos classes et la déclaration de là où les méthodes auxquelles vous voudriez adjoindre des callbacks et une petite modification de celles-ci.
CODE
> talkin = Talking.new > talkin.sentence = "I talk to the wind" > talkin.talk > "I talk to the wind"
CODE
> talkin = WiseTalking.new > talkin.sentence = "I talk to the wind" > talkin.talk > "***Thinking*** ***meditate***" > "I talk to the wind" > "But I do not hold the truth"
Réalisation
Nous utiliserons ce même mécanisme pour réaliser les callbacks au runtime. Cette capacité sera conférée à vos modèles par l’inclusion d’un module, qui permettra à chaque instance de créer sa propre chaine de callbacks.
CODE
Étant donné que les callbacks ne peuvent être définis que sur des classes. Nous allons créer pour chaque instance qui appelle #callbacks une classe qui hérite de ChainBase. En Ruby, les classes ne sont que des instances de la classe Class. Ce qui explique la syntaxe Class.new(ChainBase). Passer une classe existante en paramètre au constructeur de Class en fait la classe parente (d’où hérite la classe en cours de création). Notons que cette syntaxe crée une classe anonyme, qui prendra le nom de la première constante à laquelle elle se verra attribuée. Nous pouvons dors et déjà créer des callbacks
class Message < ActiveRecord::Base include RuntimeCallbacks end > m = Message.new > m.callbacks.after_save :do_something > m.callbacks._save_callbacks 1 => [#<ActiveSupport::Callbacks::Callback:0x007fc08db46338 @klass=#<Class:0x007fc08db4dd90>, @kind=:after, @chain=[...], @per_key={:if=>[], :unless=>[]}, @options={:prepend=>true, :if=>["!halted && value != false"], :unless=>[]}, @raw_filter=:do_something, @filter=:do_something, @compiled_options="true && (!halted && value != false)", @callback_id=25753>]
Cependant, les callbacks sont définis au niveau des classes, mais toujours exécutés sur des instances. Il nous faut donc également une instance de notre classe qui descend de ChainBase.
CODE
Nous allons faire en sorte que cette chaine de callbacks soit exécutée. Pour ce faire, il faudra qu’on ajoute à la classe originelle un callback unique par type de callback qui déclenche la chaine de callbacks au runtime, si cette chaine existe.
CODE
Verbeux certes, mais facile à refactoriser et surtout fonctionnel :
class Message < ActiveRecord::Base include RuntimeCallbacks end > message = Message.new > message.callbacks.after_validation{ puts "Runtime callback works !" } > message.save => "Runtime callback works !"
Mais il existe un gros problème. L’instance où sont exécutés ces callbacks n’a aucune connexion avec le modèle. Ce qui empêche de lancer des callbacks sur des méthodes comme message.callbacks.after_validation :some_method
Nous pouvons y résoudre de manière extrêmement simple en faisant de cette instance un décorateur de l’instance première.
CODE
Les instances des sous-classes de ChainBase délégueront ainsi tous les appels qu’elles ne savent pas interpréter à l’instance originale.
Premières améliorations
Commençons par soigner l’interface. Cachons `.callbacks` et exposons à sa place les différents callbacks (after_save, before_validation...). C’est d’ailleurs le moment d’isoler leur liste :
CODE
Nous utilisons ici le module Forwardable pour transférer les appels aux méthodes after_* / before_* au produit de la méthode callbacks, qui devient privée.
Au runtime, vraiment ?
Notons que l’usage du mot runtime ou dire à l’exécution manque de précision, car même les callbacks définis dans la déclaration de la classe sont dynamiquement créés lors du chargement du fichier de code du modèle.
Conclusion
Tant d’améliorations peuvent être discutées et apportées à notre solution, mais l’idée est là : alléger vos modèles et placer la logique à l’endroit exact où s’effectue la modification des données qui en dépend.
Notes: 1 - Quand un callback est initialisé sur une classe, le mécanisme des callbacks expose sur les instances une méthode semi-privée qui permet de débugger la chaine des callbacks. Comme #_save_callbacks dans notre exemple. 2 - Parfois, les solutions intelligentes ne sont pas les meilleurs. On aurait pu utiliser une expression telle que ACTIONS.map{ |mode| [:”before_#{mode}”, :”after_#{mode}”, :”around_#{mode}”] }.flatten - [:around_validation], pour évaluer la liste des callbacks, mais avouez que c’est ainsi plus agréable à lire.
0 notes
aritylabs · 9 years
Text
Des contrôleurs optionnels grâce à const_missing !
Ah les contrôleurs, mieux ils sont fait, plus nombreux, semblables et inutiles ils seront. Ne gagnerait-on donc pas à gommer leurs ressemblances et les rendre optionnels ?
Tumblr media
Mais qu’est-ce qu’un bon contrôleur ?
Quand cette question est soulevée, l’adage skinny controllers, fat models surgit. Moi je dirais optionnal controllers, object-oriented models.
Avant tout, le bon controlleur doit respecter le cadre logiciel dans lequel il évolue. Ruby on Rails a fait le choix de populariser le style d’architecture REST, qui consiste à considérer ce qu��on délivre via son application comme des représentations de ressources avec lesquelles on peut interagir à l’aide de verbes définies par le protocole HTTP. Un controlleur ne doit donc exposer qu’une liste restreinte d’actions qui correspondent aux verbes qui permettent d’interagir avec ces ressources.
En ce sens, l’ensemble des contrôleurs constitue l’interface visible d’une application qui s’exprime en ressources.
Qu’est-ce qu’une ressource ?
Ces ressources tendent à se confondre avec les modèles, mais cela s’avère vite trompeur. Car si vous y restreignez votre application, vous finirez bien vite par ajouter des actions supplémentaires à vos contrôleurs, violant ainsi les préceptes de REST.
Prenons le classique modèle User ! Si vous n’y voyez qu’une seule ressource, vous allez devoir ajouter des actions register, confirm_email, change_password, reset_password, confirm_reset_password alors que si vous considérez le mot de passe comme une resource, et l’inscription (registration) comme une autre, le tout s’emboite joliment dans des contrôleurs séparés faciles à écrire. Ou à “abstracter” comme nous verrons dans le reste de l’article !
Réalisation
Voici ce à quoi peut ressembler un contrôleur type (une version réduite à l’essentiel d’un scaffold)
CODE
Nous pouvons assez facilement le rendre générique et en faire hériter d’autres contrôleurs.
CODE
Dans la méthode resource_class , nous détectons en fonction du nom du contrôleur en cours (celui qui hérite de resources_controller) le nom de la classe associée et nous l’utilisons dans les différentes actions
Nous pouvons dès lors écrire des contrôleurs vides, qui ne contiennent que la déclaration :
CODE
Mais ce travail a déjà été fait, avec brio, sur tout le gem inherited_resources que nous utiliserons plus tard s’il est utilisé par l’application.
N’y aurait-il pas un moyen de se débarrasser de cette déclaration vide ?
Si, il y a const_missing!
Vous connaissez certainement method_missing, la méthode qui s’exécute quand un objet reçoit un message auquel il ne sait répondre, ni lui ni ses ancêtres dans la hiérarchie de classes.
const_missing est son corolaire pour les constantes, mais joue un rôle important : celui du chargement de classes. Nous verrons cela en détail dans la dernière partie de l’article. Concentrons-nous sur const_missing.
Cette fonction peut être définie au niveau des modules mais vous voulons dans notre cas la rendre globale. C’est donc dans la class Object que nous allons la définir en tant que méthode de classe.
CODE
Nous limitons notre intervention aux seuls noms de classes qui finissent par Controller et même dans ce cas-là nous donnons une chance à l’autoload (voir ci-après) dans le cas où le contrôleur existe, mais n’a juste pas encore été chargé.
CODE
Nous pouvons alors définir notre contrôleur tout en nous assurant que le modèle correspondant existe. Faisons cela dans la classe MissingController proprement dite :
CODE
CODE
That’s it ! Il ne nous reste maintenant qu’à donner vie à ces contrôleurs via les routes. Vous pouvez limiter les actions exposées via l’attribute only:
CODE
const_missing et autoload
const_missing est la pierre angulaire du mécanisme d’autoload, qui rend les classes de notre code disponibles partout sans le moindre require. Ruby embarque autoload que Ruby on Rails améliore pour permettre le chargement de classes au runtime et de définir différents paths (emplacements) d’où pourront être chargées les classes.
Le mécanisme de recherche de constantes en Ruby suit des règles simples pour déterminer l’emplacement de la bonne constante à utiliser à un endroit donné de votre code. Ce mécanisme repose sur la méthode Module.nesting qui renvoie le niveau d’imbrication de modules où elle est appelée. Votre constante est recherchée au sein de ces modules, ou dans les ancêtres du premier module (Module.nesting.first.ancestors) ou dans les ancêtres de object (Object.ancestors)
Si aucun de ces emplacements ne contient la constate convoitée, le processus d’autoload (chargement automatique de constante) est déclenché.
0 notes
aritylabs · 9 years
Text
Expérimentation : les symboles Ruby, Singleton, Eigenclass et valeurs immédiates !
En Ruby, nous allons réaliser le patron de conception Singleton de manière... singulière !
Le singleton
Le Singleton est en programmation orientée objet un mécanisme qui permet de n’instantier qu’une seule fois une classe donnée. L’usage typique serait celui d’une classe qui ouvre une connexion permanente à un SGBD.
Par essence, un :symbol Ruby renvoie toujours le même objet qui symbolise (à juste titre) la chaine de caractère qui le nomme.
Pour vous en convaincre, essayez :
CODE
La similitude saute aux yeux ! Réalisation.
Les symbols en guise de Singleton
CODE
Si une méthode non connue de Symbol est appelée, nous regardons si une constante du même nom que la chaine du symbol existe, nous l'instancions et appelons sur elle la méthode du même nom, avec les mêmes arguments. Nous prenons garde à n'instancier qu'une fois cette classe grâce au sacro-saint principe du class-cache.
L'utilisation se fera comme suit : :MaClass.ma_method(monargument). Pour les classes sous espace de nom, des quotes suffisent :"NS::MaClass".ma_method
Mais cette implémentation a un problème, nous ne pouvons pas passer des arguments au constructeur de la méthode, c'est pourquoi une méthode explicite est nécessaire 
CODE
Les symbols comme factory de singletons
C'est parfait, chaque représentation d'une classe par un symbol peut servir d'instance unique. Mais poussons l'expérimentation plus loin et permettons au symbol d'héberger les méthodes de sa propre classe. Faisons en une sorte de factory de singletons, qui ressemblerait à cela.
CODE
:SGBDConnection.my_method => "That's me" :AnotherSymbol.my_method NameError: undefined local variable or method `my_method' for main:Object
Eigenclass
Tout objet Ruby peut disposer d'une classe cachée où peuvent lui être ajoutées des méthodes qui ne seront pas accessibles aux autres instances du même type.
Et cette classe cachée a un nom, et même plusieurs : le plus connu étant "eigenclass", qui vient de l'allemand et veut à peu près dire "sa propre propriété à lui" lol. Moins utilisé, mais plus officiel, son autre nom n'est autre que "class singleton".
Le symbole serait-il donc non seulement une instance unique mais disposerait-il aussi d'une classe unique cachée où nous pourront mettre nos méthodes du symbol ? Idéal pour réaliser le singleton sans même déclarer de nouvelles classes !?
Essayons pour voir
CODE
Vous avez surement pu noté la drôle de syntaxe class << self. Ce n'est pas un double héritage (!) mais la porte d'entrée vers l'eigenclass de tout object que vous metteriez à la place de self.
Ruby 1.9 et supérieur offre la méthode Object#singleton_class qui permet d'accéder à l'eigenclass de manière plus commode, mais avouons-le, moins stylée !
Valeurs immédiates
Exécutons donc ce code : 
:MySymbol.singleton do    def new_method     "return"   end end
1.9.3-p429 :003 > :my_folk.singleton_class
TypeError: can't define singleton
Que se passe-t-il ? On nous aurait menti ? Vérifions : 
1.9.3-p429 :003 > :MySymbol.singleton_class
TypeError: can't define singleton
Les symboles serait-il des objects bridées ou de... classe inférieure ? En quelques sortes oui, car ils font partie, avec les "petits" entiers (instances de Fixnum) des "valeurs immédiates". On ne peut pas dire s'ils sont manipulés par référence ou par valeur. Leurs classes n'offrent aucune méthode "muatrice" puisqu'ils sont immuables et non duplicables.
La conséquence de cela est qu'ils n'ont pas droit à leur eigenclass. Ce qui est, pour notre démonstration de force sur le singleton, bien dommage !
Factory de singletons quand même
Mais rien ne nous empêche de tenir la promesse formulée là-dessus, de faire des symbols un foyer de classes singletons.
Conclusion, je dirais que cette article n'a d'autres prétention que de vous faire découvrir quelques notions probablement méconnu jusque là, à bientôt.
0 notes