How to implement a dynamic attribute default in chef LWRP definition - ruby

I would like to be able to define a lightweight resource with let's say 3 parameters, two of them being basic/elementary parameters and the third being a combination of these two. I would also like to provide a possibility of customization of the third parameter. For example:
How to modify following code to achieve above behaviour for the full_name attribute:
resource definition:
actions :install
attribute :name, :kind_of => String, :name_attribute => true
attribute :version, :kind_of => String
attribute :full_name, :kind_of => String
provider definition:
action :install do
Chef::Log.info "#{new_resource.full_name}"
end
I would like to see different outputs for different resource directives, e.g.:
resource "abc" do
version "1.0.1"
end
will result in abc-1.0.1, but:
resource "def" do
version "0.1.3"
full_name "completely_irrelevant"
end
will result in completely_irrelevant.
Is there a possibility to define this behaviour in the resource definition (probably through the default parameter) or I am able to do it in provider definition only? If the second is true, then can I store the calculated value in the new_resource object's full_name attribute (the class seems to miss the full_name= method definition) or I have to store it in a local variable?
Update
Thanks to Draco's hint, I realized that I can create an accessor method in the resource file and calculate the full_name value on the fly when requested. I would prefer a cleaner solution but it's much better than calculating it in action implementation.
Chef version
Chef: 10.16.4

Setting #full_name in constructor, similar to providing default action in chef < 0.10.10, as written in wiki, does not work, because #version is not set at that point yet.
def initialize( name, run_context=nil )
super
#full_name ||= "%s-%s" % [name, version]
end
So we have to overwrite full_name method in resource by adding
def full_name( arg=nil )
if arg.nil? and #full_name.nil?
"%s-%s" % [name, version]
else
set_or_return( :full_name, arg, :kind_of => String )
end
end
into resource definition. That works. Tested.

attribute :full_name, :kind_of => String, default => lazy {|r| "#{r.name}-#{r.version}" }

After fighting this for some time, I found this to work cleanly.
attribute :eman, String, default: lazy {|r| r.name.reverse }
The part that was missing for me was the |r| parameter to the lazy block.
https://docs.chef.io/resource_common.html#lazy-evaluation

Related

chef -- pass an attribute hash to a resource

chef has many resources\providers\definitions, for each of which there are attributes that can be set. for instance, see this and this.
by examine few definitions, it is cleat the the attributes given for a specific resource\provider\definition are packed into a hash pointed by the param variable.
i was wondering whether there is the ability to use a resource\provider\definition without unpacking a hash. here is a pseudo-code or my intentions:
attr = { :name => "/tmp/folder", :owner => "root", :group => "root", :mode => 0755, :action => :create }
directory attr
instead of writing it as follows:
directory "/tmp/folder" do
owner "root"
group "root"
mode 0755
action :create
end
is there a native way of achieving something alike?
thanks you, roth.
You can try the following
attrs = { .. }
directory "/tmp/folder" do
attrs.each do |method_name, value|
send(method_name, value)
end
end
More about Ruby's send: http://apidock.com/ruby/Object/send
by examine few definitions, it is cleat the the attributes given for a specific resource\provider\definition are packed into a hash pointed by the param variable.
This is only true for definitions.
In the case of resources the common attributes (retries/actions/etc) are a mixture of attributes and methods in the Chef::Resource class (super class of all resources). For the resource specific attributes they are typically defined as methods on the resource in question. In the case of LWRPs Chef will generate a class behind the scenes and add each of the attributes as methods to that class.
i was wondering whether there is the ability to use a resource\provider\definition without unpacking a hash.
The Chef::Resource class has a json_create method, so assuming you converted your hash to JSON it may be possible. More generally I'm curious as to the reason for wanting to do this as I believe that it will make your recipes harder to understand.

Freeze a property in DataMapper Model

Consider I have this following model definition, I want a particular property which should be constant from the moment it has been created
class A
property :a1, String, :freeze => true
end
Is there something like this? or may be using callbacks ?
Try the following:
class YourModel
property :a1, String
def a1=(other)
if a1
raise "A1 is allready bound to a value"
end
attribute_set(:a1, other.dup.freeze)
end
end
The initializer internally delegates to normal attribute writers, so when you initialize the attribute via YourModel.new(:a1 => "Your value") you cannot change it with your_instance.a1 = "your value".. But when you create a fresh instance. instance = YourModel.new you can assign once instance.a1 = "Your Value".
If you don't need to assign the constant, then
property :a1, String, :writer => :private
before :create do
attribute_set :a1, 'some value available at creation time'
end
may suffice

field vs method ruby on rails

I have this class:
class User
include Mongoid::Document
field :revenues, :type => Integer, :default => nil
attr_accessible :revenues
#now method
def revenues
return 1
end
end
Why in console I get 1 instead nil?
1.9.3-p125 :002 > u.revenues
=> 1
Which has priority, the method or the field? How can I created a method with the same features that a field?
The field macro is defined in Mongoid::Document. It is neither a syntatic feature from Ruby nor from Rails.
What's happening with your code is the following:
The field function creates for you some methods, one of them is called revenues.
When you create another method called revenues, you are in effect overwriting the previously defined method, therefore making it useless.
Short answer: I don't understand a zip about Mongoid, but chances are that your field still exists even after you defined oce again a method named revenues. The only drawback is that you cannot access it by calling myUser.revenues anymore.
Try to make a test: access your field with the notation some_user[:revenues] and see what happen :)
Best regards

data_mapper, attr_accessor, & serialization only serializing properties not attr_accessor attributes

I'm using data_mapper/sinatra and trying to create some attributes with attr_accessor. The following example code:
require 'json'
class Person
include DataMapper::Resource
property :id, Serial
property :first_name, String
attr_accessor :last_name
end
ps = Person.new
ps.first_name = "Mike"
ps.last_name = "Smith"
p ps.to_json
produces this output:
"{\"id\":null,\"first_name\":\"Mike\"}"
Obviously I would like for it to give me both the first and last name attributes. Any ideas on how to get this to work in the way one would expect so that my json has all of the attributes?
Also, feel free to also explain why my expectation (that I'd get all of the attributes) is incorrect. I'm guessing some internal list of attributes isn't getting the attr_accessor instance variables added to it or something. But even so, why?
Datamapper has it’s own serialization library, dm-serializer, that provides a to_json method for any Datamapper resource. If you require Datamapper with require 'data_mapper' in your code, you are using the data_mapper meta-gem that requires dm-serializer as part of it’s set up.
The to_json method provided by dm-serializer only serializes the Datamapper properties of your object (i.e. those you’ve specified with property) and not the “normal” properties (that you’ve defined with attr_accessor). This is why you get id and first_name but not last_name.
In order to avoid using dm-serializer you need to explicitly require those libraries you need, rather than rely on data_mapper. You will need at least dm-core and maybe others.
The “normal” json library doesn’t include any attributes in the default to_json call on an object, it just uses the objects to_s method. So in this case, if you replace require 'data_mapper' with require 'dm-core', you will get something like "\"#<Person:0x000001013a0320>\"".
To create json representations of your own objects you need to create your own to_json method. A simple example would be to just hard code the attributes you want in the json:
def to_json
{:id => id, :first_name => first_name, :last_name => last_name}.to_json
end
You could create a method that looks at the attributes and properties of the object and create the appropriate json from that instead of hardcoding them this way.
Note that if you create your own to_json method you could still call require 'data_mapper', your to_json will replace the one provided by dm-serializer. In fact dm-serializer also adds an as_json method that you could use to create the combined to_json method, e.g.:
def to_json
as_json.merge({:last_name => last_name}).to_json
end
Thanks to Matt I did some digging and found the :method param for dm-serializer's to_json method. Their to_json method was pretty decent and was basically just a wrapper for an as_json helper method so I overwrote it by just adding a few lines:
if options[:include_attributes]
options[:methods] = [] if options[:methods].nil?
options[:methods].concat(model.attributes).uniq!
end
The completed method override looks like:
module DataMapper
module Serializer
def to_json(*args)
options = args.first
options = {} unless options.kind_of?(Hash)
if options[:include_attributes]
options[:methods] = [] if options[:methods].nil?
options[:methods].concat(model.attributes).uniq!
end
result = as_json(options)
# default to making JSON
if options.fetch(:to_json, true)
MultiJson.dump(result)
else
result
end
end
end
end
This works along with an attributes method I added to a base module I use with my models. The relevant section is below:
module Base
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def attr_accessor(*vars)
#attributes ||= []
#attributes.concat vars
super(*vars)
end
def attributes
#attributes || []
end
end
def attributes
self.class.attributes
end
end
now my original example:
require 'json'
class Person
include DataMapper::Resource
include Base
property :id, Serial
property :first_name, String
attr_accessor :last_name
end
ps = Person.new
ps.first_name = "Mike"
ps.last_name = "Smith"
p ps.to_json :include_attributes => true
Works as expected, with the new option parameter.
What I could have done to selectively get the attributes I wanted without having to do the extra work was to just pass the attribute names into the :methods param.
p ps.to_json :methods => [:last_name]
Or, since I already had my Base class:
p ps.to_json :methods => Person.attributes
Now I just need to figure out how I want to support collections.

Static local variables for methods in Ruby?

I have this:
def valid_attributes
{ :email => "some_#{rand(9999)}#thing.com" }
end
For Rspec testing right? But I would like to do something like this:
def valid_attributes
static user_id = 0
user_id += 1
{ :email => "some_#{user_id}#thing.com" }
end
I don't want user_id to be accessible from anywhere but that method,
is this possible with Ruby?
This is a closure case. Try this
lambda {
user_id = 0
self.class.send(:define_method, :valid_attributes) do
user_id += 1
{ :email => "some_#{user_id}#thing.com" }
end
}.call
Wrapping everything in lambda allows the variables defined within lambda to only exist in the scope. You can add other methods also. Good luck!
This answer is a little larger in scope than your question, but I think it gets at the root of what you're trying to do, and will be the easiest and most maintainable.
I think what you're really looking for here is factories. Try using something like factory_girl, which will make a lot of testing much easier.
First, you'd set up a factory to create whatever type of object it is you're testing, and use a sequence for the email attribute:
FactoryGirl.define do
factory :model do
sequence(:email) {|n| "person#{n}#example.com" }
# include whatever else is required to make your model valid
end
end
Then, when you need valid attributes, you can use
Factory.attributes_for(:model)
You can also use Factory.create and Factory.build to create saved and unsaved instances of the model.
There's explanation of a lot more of the features in the getting started document, as well as instructions on how to add factories to your project.
You can use a closure:
def validator_factory
user_id = 0
lambda do
user_id += 1
{ :email => "some_#{user_id}#thing.com" }
end
end
valid_attributes = validator_factory
valid_attributes.call #=> {:email=>"some_1#thing.com"}
valid_attributes.call #=> {:email=>"some_2#thing.com"}
This way user_id won't be accessible outside.
I'd use an instance variable:
def valid_attributes
#user_id ||= 0
#user_id += 1
{ :email => "some_#{#user_id}#thing.com" }
end
The only variables Ruby has are local variables, instance variables, class variables and global variables. None of them fit what you're after.
What you probably need is a singleton that stores the user_id, and gives you a new ID number each time. Otherwise, your code won't be thread-safe.

Resources