Monthly trending Ruby on Rails repositories. June, 24
[vc_row][vc_column][vc_column_text]Some people say that Summer is a low period when most of the people take vacations and the overall activity is dying. The same theory might be applied to developers. But here is a contrasting point – developers are passionate people and they will hardly...
Weekly trending Ruby on Rails repositories. June, 11
Summer is the time when most of the tech and software development conferences are held. RubyKaigi (for example) that lasted from May 31 to June 2 brought many interesting solutions and insights to Ruby and RoR worlds. Today we will tell you about some of them like Kiba or Rib which have been inc...
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.
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.
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.
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 :
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.
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.
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.
My current project involves groups, users, and granting groups access to a user's requests. I've got a service object that lets me do this, and today I realized I shouldn't let this service object be instantiated with a particular user and group unless a user has a membership to that group. Well it turns out it's super easy to add active model style validations to plain ruby class.
class Foo
include ActiveModel::Validations
attr_accessor :bar
validates_presence_of :bar
end
And that's all it takes.
For more information check out this sweet post by Yehuda Katz: http://yehudakatz.com/2010/01/10/activemodel-make-any-ruby-object-feel-like-activerecord/
Today we studied the chapter #2 of Crafting Rails 4 Applications from José Valim, we created a sample ContactForm with validations and compatible with standard Rails form_for.
Submitting form data is a common feature of web applications -- allowing users to submit their information and giving them feedback whether the information is valid or not. [ActiveRecord](http://api.rubyonrails.org/classes/ActiveRecord/Base.html) comes with a powerful set of validators for attributes on a persisted data model. When data is not persisted, or used for other non-active record purposes, [Active Model Helper Modules](http://api.rubyonrails.org/classes/ActiveModel.html) reduce the complexity of validations on your plain old Ruby objects. ## Routing Create the routes needed for displaying the form object and posting the data + Restrict resources to the routes you need using `only:`
## Controller and Actions Create a controller with `new` and `create` actions. + `respond_with` will re-render the `new` action if there are any validation errors on the model + If there are no errors on the model the visitor will be redirected to `show` the current resource. In this case the user will be redirected to `some_other_success_path`
# app/controllers/registration_controller.rb class RegistrationsController < ApplicationController respond_to :html def new @registration = Registration.new end def create @registration = Registration.new(registration_params) @registration.register respond_with @registration, location: some_success_path end private def registration_params # ... end end
## View with Registration Form The view renders a web form with fields to submit. + Use the ActiveModel object `@registration` in the form + Form generates the endpoint `registration_path` and method of delivery `post` + Validation errors will display inline within the form just like ActiveRecord
## Object with ActiveModel Conversion, Naming, and Validations Use any of the [ActiveRecord Validations](http://guides.rubyonrails.org/active_record_validations_callbacks.html) in the model. + Command pattern used when calling `register` method. + ActiveRecord validation syntax on attributes. + ActiveModel::Model mixin includes modules, and includes an initialization method.
# app/models/registration.rb class Registration include ActiveModel::Model attr_accessor( :company_name, :email, :first_name, :last_name, :terms_of_service ) validates :company_name, presence: true validates :email, presence: true, email: true validates :first_name, presence: true validates :last_name, presence: true validates :terms_of_service, acceptance: true def register if valid? # Do something interesting here # - create user # - send notifications # - log events, etc. end end private def create_user # ... end end
### Takeaways + Keep business logic out of the Controller and Views + Add validation support to plain Ruby object using ActiveModel includes + Display data validation errors in the form + Use ActiveModel naming conventions for generating form endpoints