structure 3.6.3 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3ecf2906322a2c0abaa86ab756c7154139788867b214ee5b30a071adf5e044ff
4
- data.tar.gz: c36c7b4dfc5656a74b056c72af11ab478fc97886a1638312dd389d252e1977b3
3
+ metadata.gz: 2306964e18c5a78a2241c8c42071fd0a1921fa8c7c54cf210b0825b20607cfca
4
+ data.tar.gz: c43cf42996e715fe5c1ca1b57bf91ee411405f188382b2d10c0b593a66e90a6f
5
5
  SHA512:
6
- metadata.gz: f6bcd31e925af5a25bb65b9ee1b2c4eff4654b7fbce6e7110d3c89195744b5ed7f3564a33dcf0b0cf19a373df315d955637388ccad8f6fa19aa394b21234e1d5
7
- data.tar.gz: 3707eb84b1b0e702f4e13cf90b9048a3858ed71b07e399d969db4e4e36523fa4e9860c38fbe0d33a4b11336fb2b9f7389d136e80fa4ed280cd0301297102de56
6
+ metadata.gz: 314242bf300d7ec09e1755647d4a8fef224eb2c64fc0b6d945be386b7af008c22146e80993f1e753f1df9cc5b37841242b7ab4a14d71cde42732952e288fc196
7
+ data.tar.gz: 72dcc21770b4f57b67fd64b8ad69695f02d0a55b2622717f63795b8da32aa8b45cd61db146b2e2c4dc02683588ca74dba831437652afaed9bb25fc159195d0b5
@@ -5,12 +5,15 @@ require "structure/types"
5
5
  module Structure
6
6
  # Builder class for accumulating attribute definitions
7
7
  class Builder
8
- attr_reader :mappings, :types, :defaults, :after_parse_callback
8
+ # @api private
9
+ attr_reader :mappings, :defaults, :types, :after_parse_callback
9
10
 
11
+ # @api private
10
12
  def initialize
11
13
  @mappings = {}
12
- @types = {}
13
14
  @defaults = {}
15
+ @types = {}
16
+ @optional = Set.new
14
17
  end
15
18
 
16
19
  # DSL method for defining attributes with optional type coercion
@@ -43,6 +46,25 @@ module Structure
43
46
  end
44
47
  end
45
48
 
49
+ # DSL method for defining optional attributes (key can be missing from input hash)
50
+ #
51
+ # @param name [Symbol] The attribute name
52
+ # @param type [Class, Symbol, Array, nil] Type for coercion (e.g., String, :boolean, [String])
53
+ # @param from [String, nil] Source key in the data hash (defaults to name.to_s)
54
+ # @param default [Object, nil] Default value if attribute is missing
55
+ # @yield [value] Block for custom transformation
56
+ # @raise [ArgumentError] If both type and block are provided
57
+ #
58
+ # @example Optional attribute
59
+ # attribute? :age, Integer
60
+ #
61
+ # @example Optional with default
62
+ # attribute? :status, String, default: "pending"
63
+ def attribute?(name, type = nil, from: nil, default: nil, &block)
64
+ attribute(name, type, from: from, default: default, &block)
65
+ @optional.add(name)
66
+ end
67
+
46
68
  # Defines a callback to run after parsing
47
69
  #
48
70
  # @yield [instance] Block that receives the parsed instance
@@ -56,14 +78,21 @@ module Structure
56
78
  @after_parse_callback = block
57
79
  end
58
80
 
59
- def attributes
60
- @mappings.keys
61
- end
81
+ # @api private
82
+ def attributes = @mappings.keys
83
+
84
+ # @api private
85
+ def optional = @optional.to_a
86
+
87
+ # @api private
88
+ def required = attributes - optional
62
89
 
63
- def coercions(context_class = nil)
64
- @types.transform_values { |type| Types.coerce(type, context_class) }
90
+ # @api private
91
+ def coercions(context = nil)
92
+ @types.transform_values { |type| Types.coerce(type, context) }
65
93
  end
66
94
 
95
+ # @api private
67
96
  def predicate_methods
68
97
  @types.filter_map do |name, type|
69
98
  if type == :boolean
data/lib/structure/rbs.rb CHANGED
@@ -15,10 +15,15 @@ module Structure
15
15
  # @type var meta: Hash[Symbol, untyped]
16
16
  meta = klass.respond_to?(:__structure_meta__) ? klass.__structure_meta__ : {}
17
17
 
18
+ attributes = meta[:mappings] ? meta[:mappings].keys : klass.members
19
+ types = meta.fetch(:types, {}) # steep:ignore
20
+ required = meta.fetch(:required, attributes) # steep:ignore
21
+
18
22
  emit_rbs_content(
19
- class_name: class_name,
20
- attributes: meta.fetch(:attributes, klass.members),
21
- types: meta.fetch(:types, {}), # steep:ignore
23
+ class_name:,
24
+ attributes:,
25
+ types:,
26
+ required:,
22
27
  has_structure_modules: meta.any?,
23
28
  )
24
29
  end
@@ -44,7 +49,7 @@ module Structure
44
49
 
45
50
  private
46
51
 
47
- def emit_rbs_content(class_name:, attributes:, types:, has_structure_modules:)
52
+ def emit_rbs_content(class_name:, attributes:, types:, required:, has_structure_modules:)
48
53
  # @type var lines: Array[String]
49
54
  lines = []
50
55
  lines << "class #{class_name} < Data"
@@ -58,7 +63,11 @@ module Structure
58
63
  [attr, rbs_type != "untyped" ? "#{rbs_type}?" : rbs_type]
59
64
  end.to_h
60
65
 
61
- keyword_params = attributes.map { |attr| "#{attr}: #{rbs_types[attr]}" }.join(", ")
66
+ # Mark optional attributes with ? prefix in keyword params
67
+ keyword_params = attributes.map do |attr|
68
+ prefix = required.include?(attr) ? "" : "?"
69
+ "#{prefix}#{attr}: #{rbs_types[attr]}"
70
+ end.join(", ")
62
71
  positional_params = attributes.map { |attr| rbs_types[attr] }.join(", ")
63
72
 
64
73
  lines << " def self.new: (#{keyword_params}) -> #{class_name}"
@@ -11,130 +11,98 @@ module Structure
11
11
 
12
12
  # Main factory method for creating type coercers
13
13
  #
14
- # @param type [Class, Symbol, Array, String] Type specification
15
- # @return [Proc, Object] Coercion proc or the type itself if no coercion available
14
+ # @param type [Class, Symbol, Array, String, nil] Type specification
15
+ # @param context [Class, nil] Context class for lazy-loading and self-referential types
16
+ # @return [Proc, nil] Coercion proc or nil if no coercion needed
17
+ # @raise [ArgumentError] If type is a Hash instance (typed hashes not yet supported) or unsupported type
16
18
  #
17
19
  # @example Boolean type
18
20
  # coerce(:boolean) # => boolean proc
19
21
  #
22
+ # @example Self-referential types
23
+ # coerce(:self) # => proc that calls context.parse
24
+ #
20
25
  # @example Kernel types
21
26
  # coerce(Integer) # => proc that calls Kernel.Integer
22
27
  #
28
+ # @example Array types
29
+ # coerce([String]) # => proc that coerces array elements to String
30
+ #
23
31
  # @example Parseable types
24
32
  # coerce(Date) # => proc that calls Date.parse
25
33
  #
26
- # @example Array types
27
- # coerce([String]) # => proc that coerces array elements to String
34
+ # @example No coercion
35
+ # coerce(nil) # => nil
36
+ #
37
+ # @example Custom lambdas
38
+ # coerce(->(val) { val.upcase }) # => returns the lambda itself
28
39
  #
29
- # @example String class name (lazy resolved)
40
+ # @example Lazy-resolved classes
30
41
  # coerce("MyClass") # => proc that resolves and coerces to MyClass
31
- def coerce(type, context_class = nil)
42
+ def coerce(type, context = nil)
32
43
  case type
33
44
  when :boolean
34
45
  boolean
35
46
  when :self
36
- self_referential
37
- when String
38
- string_class(type, context_class)
47
+ self_referential(context)
48
+ when ->(t) { t.respond_to?(:name) && t.name && Kernel.respond_to?(t.name) }
49
+ kernel(type)
39
50
  when ->(t) { t.is_a?(Array) && t.length == 1 }
40
- array(type.first, context_class)
41
- when Hash
42
- raise ArgumentError, "Cannot specify #{type.inspect} as type"
51
+ array(type.first, context)
43
52
  when ->(t) { t.respond_to?(:parse) }
44
53
  parseable(type)
45
- when ->(t) { t.respond_to?(:name) && t.name && Kernel.respond_to?(t.name) }
46
- kernel(type)
47
- when ->(t) { t.respond_to?(:call) }
48
- type
49
54
  when nil
50
55
  type
56
+ when ->(t) { t.respond_to?(:call) }
57
+ type
58
+ when String
59
+ lazy_class(type, context)
51
60
  else
52
61
  raise ArgumentError, "Cannot specify #{type.inspect} as type"
53
62
  end
54
63
  end
55
64
 
56
- def resolve_class(class_name, context_class)
57
- if context_class && defined?(context_class.name)
58
- namespace = context_class.name.to_s.split("::")[0...-1]
59
- if namespace.any?
60
- begin
61
- namespace.reduce(Object) { |mod, name| mod.const_get(name) }.const_get(class_name)
62
- rescue NameError
63
- Object.const_get(class_name)
64
- end
65
- else
66
- Object.const_get(class_name)
67
- end
68
- else
69
- Object.const_get(class_name)
70
- end
71
- rescue NameError => e
72
- raise NameError, "Unable to resolve class '#{class_name}': #{e.message}"
73
- end
74
-
75
65
  private
76
66
 
77
67
  def boolean
78
- ->(val) { BOOLEAN_TRUTHY.include?(val) }
79
- end
80
-
81
- def self_referential
82
- proc { |val| parse(val) }
68
+ @boolean ||= ->(val) { BOOLEAN_TRUTHY.include?(val) }
83
69
  end
84
70
 
85
- def kernel(type)
86
- ->(val) { Kernel.send(type.name, val) }
71
+ def self_referential(context)
72
+ ->(val) { context.parse(val) }
87
73
  end
88
74
 
89
- def parseable(type)
90
- ->(val) { type.parse(val) }
91
- end
92
-
93
- def string_class(class_name, context_class)
75
+ def lazy_class(class_name, context)
94
76
  resolved_class = nil
95
- mutex = Mutex.new
96
77
 
97
- proc do |value|
98
- unless resolved_class
99
- mutex.synchronize do
100
- resolved_class ||= Structure::Types.resolve_class(class_name, context_class)
101
- end
102
- end
103
-
104
- if resolved_class.respond_to?(:parse)
105
- resolved_class.parse(value) # steep:ignore
106
- else
107
- value
108
- end
78
+ ->(value) do
79
+ resolved_class ||= resolve_class(class_name, context)
80
+ # @type var resolved_class: untyped
81
+ resolved_class.parse(value)
109
82
  end
110
83
  end
111
84
 
112
- def array(element_type, context_class = nil)
113
- if element_type == :self
114
- proc do |value|
85
+ def array(element_type, context = nil)
86
+ case element_type
87
+ when :self
88
+ lambda do |value|
115
89
  unless value.respond_to?(:map)
116
90
  raise TypeError, "can't convert #{value.class} into Array"
117
91
  end
118
92
 
119
- value.map { |element| parse(element) }
93
+ value.map { |element| context.parse(element) }
120
94
  end
121
- elsif element_type.is_a?(String)
122
- proc do |value|
95
+ when String
96
+ lambda do |value|
123
97
  unless value.respond_to?(:map)
124
98
  raise TypeError, "can't convert #{value.class} into Array"
125
99
  end
126
100
 
127
- resolved_class = Structure::Types.resolve_class(element_type, context_class)
128
- value.map do |element|
129
- if resolved_class.respond_to?(:parse)
130
- resolved_class.parse(element)
131
- else
132
- element
133
- end
134
- end
101
+ resolved_class = resolve_class(element_type, context)
102
+ value.map { |element| resolved_class.parse(element) }
135
103
  end
136
104
  else
137
- element_coercer = coerce(element_type, context_class)
105
+ element_coercer = coerce(element_type, context)
138
106
  lambda do |value|
139
107
  unless value.respond_to?(:map)
140
108
  raise TypeError, "can't convert #{value.class} into Array"
@@ -144,6 +112,35 @@ module Structure
144
112
  end
145
113
  end
146
114
  end
115
+
116
+ def parseable(type)
117
+ @parseable_cache ||= {} # : Hash[untyped, Proc]
118
+ @parseable_cache[type] ||= ->(val) { type.parse(val) }
119
+ end
120
+
121
+ def kernel(type)
122
+ @kernel_cache ||= {} # : Hash[untyped, Proc]
123
+ @kernel_cache[type] ||= ->(val) { Kernel.send(type.name, val) }
124
+ end
125
+
126
+ def resolve_class(class_name, context)
127
+ if context && defined?(context.name)
128
+ namespace = context.name.to_s.split("::")[0...-1]
129
+ if namespace.any?
130
+ begin
131
+ namespace.reduce(Object) { |mod, name| mod.const_get(name) }.const_get(class_name)
132
+ rescue NameError
133
+ Object.const_get(class_name)
134
+ end
135
+ else
136
+ Object.const_get(class_name)
137
+ end
138
+ else
139
+ Object.const_get(class_name)
140
+ end
141
+ rescue NameError => e
142
+ raise NameError, "Unable to resolve class '#{class_name}': #{e.message}"
143
+ end
147
144
  end
148
145
  end
149
146
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Structure
4
- VERSION = "3.6.3"
4
+ VERSION = "4.0.0"
5
5
  end
data/lib/structure.rb CHANGED
@@ -26,94 +26,95 @@ module Structure
26
26
  # @type var klass: untyped
27
27
  klass = Data.define(*builder.attributes)
28
28
 
29
- # capture all metadata and attach to class - no closure capture needed
30
- mappings = builder.mappings
31
- coercions = builder.coercions(klass)
32
- predicates = builder.predicate_methods
33
- after = builder.after_parse_callback
29
+ # Override initialize to make optional attributes truly optional
30
+ optional_attrs = builder.optional
31
+ unless optional_attrs.empty?
32
+ klass.class_eval do
33
+ alias_method(:__data_initialize__, :initialize)
34
+
35
+ define_method(:initialize) do |**kwargs| # steep:ignore
36
+ optional_attrs.each do |attr|
37
+ kwargs[attr] = nil unless kwargs.key?(attr)
38
+ end
39
+ __data_initialize__(**kwargs) # steep:ignore
40
+ end
41
+ end
42
+ end
34
43
 
44
+ builder.predicate_methods.each do |pred, attr|
45
+ klass.define_method(pred) { !!public_send(attr) }
46
+ end
47
+
48
+ # Store metadata on class to avoid closure capture (memory optimization)
35
49
  meta = {
36
- attributes: builder.attributes.freeze,
37
- types: builder.types.freeze,
38
- defaults: builder.defaults.freeze,
39
- mappings: mappings.freeze,
40
- coercions: coercions.freeze,
41
- predicates: predicates.freeze,
42
- after: after,
50
+ types: builder.types,
51
+ defaults: builder.defaults,
52
+ mappings: builder.mappings,
53
+ coercions: builder.coercions(klass),
54
+ after_parse: builder.after_parse_callback,
55
+ required: builder.required,
43
56
  }.freeze
44
57
  klass.instance_variable_set(:@__structure_meta__, meta)
45
58
  klass.singleton_class.attr_reader(:__structure_meta__)
46
59
 
47
- # Define predicate methods
48
- predicates.each do |pred, attr|
49
- klass.define_method(pred) { !!public_send(attr) }
50
- end
51
-
52
60
  # recursive to_h
53
61
  klass.define_method(:to_h) do
54
- # @type var h: Hash[Symbol, untyped]
55
- h = {}
56
- klass.members.each do |m|
62
+ klass.members.to_h do |m|
57
63
  v = public_send(m)
58
- h[m] =
59
- case v
60
- when Array then v.map { |x| x.respond_to?(:to_h) && x ? x.to_h : x }
61
- when ->(x) { x.respond_to?(:to_h) && x } then v.to_h
62
- else v
63
- end
64
+ value = case v
65
+ when Array then v.map { |x| x.respond_to?(:to_h) && x ? x.to_h : x }
66
+ when ->(x) { x.respond_to?(:to_h) && x } then v.to_h
67
+ else v
68
+ end
69
+ [m, value]
64
70
  end
65
- h
66
71
  end
67
72
 
68
- # parse accepts JSON-ish hashes + kwargs override
69
- klass.singleton_class.define_method(:parse) do |data = {}, **kwargs|
73
+ # parse accepts JSON-ish hashes + optional overrides hash
74
+ # overrides is a positional arg (not **kwargs) to avoid hash allocation when unused
75
+ #
76
+ # @type self: singleton(Data) & _StructuredDataClass
77
+ # @type var final: Hash[Symbol, untyped]
78
+ klass.singleton_class.define_method(:parse) do |data = {}, overrides = nil|
70
79
  return data if data.is_a?(self)
71
80
 
72
81
  unless data.respond_to?(:merge!)
73
82
  raise TypeError, "can't convert #{data.class} into #{self}"
74
83
  end
75
84
 
76
- # @type var kwargs: Hash[Symbol, untyped]
77
- string_kwargs = kwargs.transform_keys(&:to_s)
78
- data.merge!(string_kwargs)
79
- # @type self: singleton(Data) & _StructuredDataClass
80
- # @type var final: Hash[Symbol, untyped]
81
- final = {}
82
-
83
- # @type var meta: untyped
84
- meta = __structure_meta__
85
-
86
- attributes = meta.fetch(:attributes)
87
- defaults = meta.fetch(:defaults)
88
- mappings = meta.fetch(:mappings)
89
- coercions = meta.fetch(:coercions)
90
- after = meta.fetch(:after)
91
-
92
- attributes.each do |attr|
93
- source = mappings[attr] || attr.to_s
94
- value =
95
- if data.key?(source) then data[source]
96
- elsif data.key?(source.to_sym) then data[source.to_sym]
97
- elsif defaults.key?(attr) then defaults[attr]
85
+ overrides&.each { |k, v| data[k.to_s] = v }
86
+
87
+ final = {}
88
+ mappings = __structure_meta__[:mappings]
89
+ defaults = __structure_meta__[:defaults]
90
+ after_parse = __structure_meta__[:after_parse]
91
+ required = __structure_meta__[:required]
92
+
93
+ # Check for missing required attributes
94
+ required.each do |attr|
95
+ from = mappings[attr]
96
+ next if data.key?(from) || data.key?(from.to_sym) || defaults.key?(attr)
97
+
98
+ raise ArgumentError, "missing keyword: :#{attr}"
99
+ end
100
+
101
+ mappings.each do |attr, from|
102
+ value = data.fetch(from) do
103
+ data.fetch(from.to_sym) do
104
+ defaults[attr]
98
105
  end
106
+ end
99
107
 
100
- coercion = coercions[attr]
101
- if coercion && !value.nil?
102
- # Procs (not lambdas) need class context for self-referential parsing
103
- # Lambdas and other callables use direct invocation
104
- value =
105
- if coercion.is_a?(Proc) && !coercion.lambda?
106
- instance_exec(value, &coercion) # steep:ignore
107
- else
108
- coercion.call(value)
109
- end
108
+ if value
109
+ coercion = __structure_meta__[:coercions][attr]
110
+ value = coercion.call(value) if coercion
110
111
  end
111
112
 
112
113
  final[attr] = value
113
114
  end
114
115
 
115
116
  obj = new(**final)
116
- after&.call(obj)
117
+ after_parse&.call(obj)
117
118
  obj
118
119
  end
119
120
 
@@ -4,18 +4,24 @@ module Structure
4
4
  @types: Hash[Symbol, untyped]
5
5
  @defaults: Hash[Symbol, untyped]
6
6
  @after_parse_callback: Proc?
7
+ @optional: Set[Symbol]
7
8
 
8
9
  def attribute: (Symbol name, untyped type, ?from: String?, ?default: untyped) ?{ (untyped) -> untyped } -> void
9
10
  | (Symbol name, ?from: String, ?default: untyped) ?{ (untyped) -> untyped } -> void
10
11
 
12
+ def attribute?: (Symbol name, untyped type, ?from: String?, ?default: untyped) ?{ (untyped) -> untyped } -> void
13
+ | (Symbol name, ?from: String, ?default: untyped) ?{ (untyped) -> untyped } -> void
14
+
11
15
  def after_parse: () { (Data) -> void } -> void
12
16
 
13
17
  def attributes: () -> Array[Symbol]
14
18
  def mappings: () -> Hash[Symbol, String]
15
19
  def types: () -> Hash[Symbol, untyped]
16
20
  def defaults: () -> Hash[Symbol, untyped]
17
- def coercions: (?untyped? context_class) -> Hash[Symbol, Proc]
21
+ def coercions: (?untyped? context) -> Hash[Symbol, Proc]
18
22
  def predicate_methods: () -> Hash[Symbol, Symbol]
23
+ def optional: () -> Array[Symbol]
24
+ def required: () -> Array[Symbol]
19
25
  def after_parse_callback: () -> (Proc | nil)
20
26
  end
21
27
  end
@@ -3,7 +3,7 @@ module Structure
3
3
  def self.emit: (untyped klass) -> String?
4
4
  def self.write: (untyped klass, ?dir: String) -> String?
5
5
 
6
- private def self.emit_rbs_content: (class_name: String, attributes: Array[Symbol], types: Hash[Symbol, untyped], has_structure_modules: bool) -> String
6
+ private def self.emit_rbs_content: (class_name: String, attributes: Array[Symbol], types: Hash[Symbol, untyped], required: Array[Symbol], has_structure_modules: bool) -> String
7
7
  private def self.parse_data_type: (untyped type, String class_name) -> String
8
8
  private def self.map_type_to_rbs: (untyped type, String class_name) -> String
9
9
  end
@@ -1,18 +1,25 @@
1
1
  module Structure
2
2
  module Types
3
+ interface _ParseableClass
4
+ def parse: (untyped) -> untyped
5
+ end
6
+
3
7
  BOOLEAN_TRUTHY: Array[untyped]
4
8
 
5
9
  self.@boolean: Proc
10
+ self.@kernel_cache: Hash[untyped, Proc]
11
+ self.@parseable_cache: Hash[untyped, Proc]
12
+ self.@string_class_cache: Hash[Array[untyped], Proc]
6
13
 
7
- def self.coerce: (untyped type, ?untyped? context_class) -> untyped
8
- def self.resolve_class: (String class_name, untyped? context_class) -> untyped
14
+ def self.coerce: (untyped type, ?untyped? context) -> untyped
9
15
 
10
16
  private def self.boolean: () -> Proc
11
- private def self.self_referential: () -> Proc
12
- private def self.string_class: (String class_name, untyped? context_class) -> Proc
13
- private def self.array: (untyped element_type, ?untyped? context_class) -> Proc
17
+ private def self.self_referential: (_ParseableClass context) -> Proc
18
+ private def self.lazy_class: (String class_name, untyped? context) -> Proc
19
+ private def self.array: (untyped element_type, ?untyped? context) -> Proc
14
20
  private def self.parseable: (untyped type) -> Proc
15
21
  private def self.kernel: (Class type) -> Proc
16
22
  private def self.parse: (untyped val) -> untyped
23
+ private def self.resolve_class: (String class_name, untyped? context) -> _ParseableClass
17
24
  end
18
25
  end
data/sig/structure.rbs CHANGED
@@ -1,13 +1,12 @@
1
1
  module Structure
2
2
  interface _StructuredDataClass
3
3
  def __structure_meta__: () -> {
4
- attributes: Array[Symbol],
5
4
  types: Hash[Symbol, untyped],
6
5
  defaults: Hash[Symbol, untyped],
7
6
  mappings: Hash[Symbol, String],
8
7
  coercions: Hash[Symbol, untyped],
9
- predicates: Hash[Symbol, Symbol],
10
- after: untyped
8
+ after_parse: untyped,
9
+ required: Array[Symbol]
11
10
  }
12
11
  end
13
12
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: structure
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.6.3
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hakan Ensari