Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions src/lib/y2configuration_management/salt/form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ def find_element_by(arg)
#
# scalar values, groups and collections
class FormElement
include FormElementHelpers
# @return [String] the key for the pillar
attr_reader :id
# @return [Symbol]
Expand All @@ -104,7 +105,7 @@ class FormElement
def initialize(id, spec, parent:)
@id = id
@name = spec.fetch("$name", humanize(id))
@type = spec.fetch("$type", "text").to_sym
@type = type_for(spec)
@help = spec["$help"] if spec ["$help"]
@scope = spec.fetch("$scope", "system").to_sym
@optional = spec["$optional"] if spec["$optional"]
Expand All @@ -129,6 +130,18 @@ def locator
def humanize(s)
s.split(/[-_]/).reject(&:empty?).map(&:capitalize).join(" ")
end

# Returns the type for a given form element specification
#
# @param spec [Hash] Form element specification
# @return [Symbol] Form element type
def type_for(spec)
if spec["$type"] == "text" && spec.key?("$key") && form_elements_in(spec).size <= 1
:key_value
else
spec.fetch("$type", "text").to_sym
end
end
end

# Scalar value FormElement
Expand Down Expand Up @@ -177,8 +190,6 @@ def collection_key?

# Container Element
class Container < FormElement
include FormElementHelpers

# @return [Array<FormElement>]
attr_reader :elements

Expand Down
23 changes: 12 additions & 11 deletions src/lib/y2configuration_management/salt/form_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,18 @@ module Salt
# https://www.suse.com/documentation/suse-manager-3/3.2/susemanager-best-practices/html/book.suma.best.practices/best.practice.salt.formulas.and.forms.html
class FormBuilder
INPUT_WIDGET_CLASS = {
color: Y2ConfigurationManagement::Widgets::Color,
text: Y2ConfigurationManagement::Widgets::Text,
number: Y2ConfigurationManagement::Widgets::Text,
email: Y2ConfigurationManagement::Widgets::Email,
password: Y2ConfigurationManagement::Widgets::Password,
url: Y2ConfigurationManagement::Widgets::URL,
select: Y2ConfigurationManagement::Widgets::Select,
boolean: Y2ConfigurationManagement::Widgets::Boolean,
date: Y2ConfigurationManagement::Widgets::Date,
datetime: Y2ConfigurationManagement::Widgets::DateTime,
time: Y2ConfigurationManagement::Widgets::Time
color: Y2ConfigurationManagement::Widgets::Color,
text: Y2ConfigurationManagement::Widgets::Text,
number: Y2ConfigurationManagement::Widgets::Text,
email: Y2ConfigurationManagement::Widgets::Email,
password: Y2ConfigurationManagement::Widgets::Password,
url: Y2ConfigurationManagement::Widgets::URL,
select: Y2ConfigurationManagement::Widgets::Select,
boolean: Y2ConfigurationManagement::Widgets::Boolean,
date: Y2ConfigurationManagement::Widgets::Date,
datetime: Y2ConfigurationManagement::Widgets::DateTime,
time: Y2ConfigurationManagement::Widgets::Time,
key_value: Y2ConfigurationManagement::Widgets::KeyValue
}.freeze

# Constructor
Expand Down
3 changes: 2 additions & 1 deletion src/lib/y2configuration_management/salt/form_data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,8 @@ def hash_collection_for_pillar(collection)
collection.reduce({}) do |all, item|
new_item = item.clone
key = new_item.delete("$key")
all.merge(key => data_for_pillar(new_item))
val = new_item.delete("$value") || data_for_pillar(new_item)
all.merge(key => val)
end
end

Expand Down
147 changes: 112 additions & 35 deletions src/lib/y2configuration_management/salt/form_data_reader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,37 @@ module Salt
# The format used in the pillar is slightly different from the internal representation of this
# module, so this class takes care of the conversion. However, the initial intentation is to not
# use it directly but through the {FormDate.from_pillar} class method.
#
# ## Handling collections
#
# There might be different kind of collections:
#
# * Array with simple values (strings, integers, etc).
# * Array of hashes. They allow a more complex collection.
# * Hash based collections which index is provided by the user.
#
# To simplify things in the UI layer, all collections are handled as arrays so, in the third
# case, some conversion is needed. Given an specification with three fields `$key`, `url`,
# and `license`, the collection would be stored in the Pillar like this:
#
# { "yast2" =>
# { "url" => "https://yast.opensuse.org", "license" => "GPL" }
# }
#
# Internally, it will be handled as an array:
#
# [{ "$key" => "yast2", "url" => "https://yast.opensuse.org", "license" => "GPL" }]
#
# Something similar applies to hash based simple collections which are originally like this (it
# is a hash where the keys are specified by the user):
#
# { "vers" => "4", "timeout" => "0" }
#
# It will be converted to:
#
# [{ "$key" => "vers", "$value" => "4" }, { "$key" => "timeout", "$value" => "0" }]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So in the pillar, I see 4 possibilities along 2 axes:

Outside, the collection is one of

  • Hash
  • Array

Inside, its elements are one of

  • scalar
  • group (hash)

How do we represent that in FormData?

For the "outside" axis, we always use an array, and represent a hash by embedding "$key" inside.

For the "inside", we either put plain scalars (for an array of scalars), or if we have a hash on the outside, then we use a hash which always has a "$key" element and then either the other elements, or a "$value" element if it is a hash of scalars.

Is that right?

It seems to me that we could simplify this by always having a hash at the inside, with an array of scalars being represented as [{ "$value" => "one" }, { "$value" => "two" }, ...]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you are right. I will try that approach.

class FormDataReader
include Yast::Logger
# @return [Form] Form definition
attr_reader :form
# @return [Pillar]
Expand All @@ -48,57 +78,104 @@ def initialize(form, pillar)
#
# @return [FormData] Form data object
def form_data
data_from_pillar = data_for_element(form.root, pillar.data)
FormData.new(form, data_from_pillar)
data_from_pillar = { "root" => hash_from_pillar(pillar.data, form.root.locator) }
defaults = defaults_for_element(form.root)
FormData.new(form, simple_merge(defaults, data_from_pillar))
end

private

# Builds a hash to keep the form element data
# Extracts data from the pillar
#
# @param element [Y2ConfigurationManagement::Salt::FormElement]
# @param data [Hash] Pillar data
# @return [Hash]
def data_for_element(element, data)
if element.is_a?(Container)
defaults = element.elements.reduce({}) { |a, e| a.merge(data_for_element(e, data)) }
{ element.id => defaults }
# @param data [Hash] Pillar data
# @param locator [FormElementLocator] Locator
# @return [Hash<String, Object>]
def data_from_pillar(data, locator)
element = form.find_element_by(locator: locator.unbounded)
case element
when Collection
collection_from_pillar(data, locator)
when Container
hash_from_pillar(data, locator)
else
value = find_in_pillar_data(data, element.locator.rest) # FIXME: remove '.root'
value ||= element.default
{ element.id => data_from_pillar_collection(value, element) }
data
end
end

# Converts from a Pillar collection into form data
# Reads a hash from the pillar for a given locator
#
# Basically, a collection might be an array or a hash. The internal representation, however,
# is always an array, so it is needed to do the conversion.
# @param data [Hash] Pillar data
# @param locator [FormElementLocator] Element locator
# @return [Hash<String, Object>]
def hash_from_pillar(data, locator)
data.reduce({}) do |all, (k, v)|
all.merge(k => data_from_pillar(v, locator.join(k.to_sym)))
end
end

# Converts a collection from the pillar
#
# @param element [Y2ConfigurationManagement::Salt::FormElement]
# @param value [Array,Hash]
# @return
def data_from_pillar_collection(collection, element)
return nil if collection.nil?
return collection unless element.respond_to?(:keyed?) && element.keyed?
collection.map do |k, v|
{ "$key" => k }.merge(v)
# @param data [Hash] Pillar data
# @param locator [FormElementLocator] Element locator
# @return [Array<Hash>]
def collection_from_pillar(data, locator)
element = form.find_element_by(locator: locator.unbounded)
if element.keyed?
data.map { |k, v| { "$key" => k }.merge(hash_from_pillar(v, locator.join(k))) }
elsif element.prototype.is_a?(FormInput)
if element.prototype.type == :key_value
data.map { |k, v| { "$key" => k, "$value" => v } }
else
data
end
else
data.map { |d| hash_from_pillar(d, locator) }
end
end

# Extracts default values for a given element
#
# @param element [FormElement]
# @return [Object]
def defaults_for_element(element)
case element
when Container
defaults = element.elements.reduce({}) { |a, e| a.merge(defaults_for_element(e)) }
{ element.id => defaults }
when Collection
{ element.id => defaults_for_collection(element) }
else
{ element.id => element.default }
end
end

# Finds a value within a Pillar
# Extracts default values for a given collection
#
# @todo This API might be available through the Pillar class.
# @param collection [Collection]
# @return [Array<Hash>]
def defaults_for_collection(collection)
if collection.keyed?
collection.default.map { |k, v| { "$key" => k }.merge(v) }
elsif collection.prototype.is_a?(FormInput) && collection.prototype.type == :key_value
collection.default.map { |k, v| { "$key" => k, "$value" => v } }
else
collection.default
end
end

# Simple deep merge
#
# @param data [Hash,Array] Data structure from the Pillar
# @param locator [FormElementLocator] Value locator
# @return [Object] Value
def find_in_pillar_data(data, locator)
return nil if data.nil?
return data if locator.first.nil?
key = locator.first
key = key.is_a?(Symbol) ? key.to_s : key
find_in_pillar_data(data[key], locator.rest)
# @param defaults [Hash] Default values
# @param data [Hash] Pillar data
def simple_merge(defaults, data)
defaults.reduce({}) do |all, (k, v)|
next all.merge(k => v) if data[k].nil?
if v.is_a?(Hash)
all.merge(k => simple_merge(defaults[k], data[k]))
else
all.merge(k => data[k])
end
end
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ def ==(other)
#
# @return [FormElementLocator]
def unbounded
self.class.new(parts.reject { |i| i.is_a?(Integer) })
self.class.new(parts.select { |i| i.is_a?(Symbol) })
end
end
end
Expand Down
1 change: 1 addition & 0 deletions src/lib/y2configuration_management/widgets.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
require "y2configuration_management/widgets/email"
require "y2configuration_management/widgets/form"
require "y2configuration_management/widgets/group"
require "y2configuration_management/widgets/key_value"
require "y2configuration_management/widgets/password"
require "y2configuration_management/widgets/select"
require "y2configuration_management/widgets/text"
Expand Down
20 changes: 15 additions & 5 deletions src/lib/y2configuration_management/widgets/collection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,7 @@ def selected_row
# @return [Array<Array<String|Yast::Term>>]
def format_items(items_list)
items_list.each_with_index.map do |item, index|
values = if item.is_a? Hash
headers_ids.map { |h| item[h] }
else
[item]
end
values = item.is_a?(Hash) ? format_hash_item(item) : [item]
formatted_values = values.map { |v| format_value(v) }
Item(Id(index.to_s), *formatted_values)
end
Expand Down Expand Up @@ -167,6 +163,20 @@ def format_value(val)
# TRANSLATORS: items count in a list
format(n_("%s item", "%s items", val.size), val.size)
end

# Returns a list of the hash values to be shown in the table
#
# When it is a text dictionary it returns a formatted string with the
# $key and the $value
#
# @return [Array<String>]
def format_hash_item(item)
if item.keys == ["$key", "$value"]
["#{item["$key"]}: #{item["$value"]}"]
else
headers_ids.map { |h| item[h] }
end
end
end
end
end
Loading