44such as converting bounding boxes to polygon representations.
55"""
66
7+ import re
78from typing import Any , Dict , List , Optional , Set , Union
89
910from stac_fastapi .core .models .patch import ElasticPath
10- from stac_fastapi .types .stac import Item , PatchAddReplaceTest , PatchRemove
11+ from stac_fastapi .types .stac import (
12+ Item ,
13+ PatchAddReplaceTest ,
14+ PatchOperation ,
15+ PatchRemove ,
16+ )
1117
1218MAX_LIMIT = 10000
1319
@@ -166,29 +172,149 @@ def merge_to_operations(data: Dict) -> List:
166172 return operations
167173
168174
169- def add_script_checks (source : str , op : str , path : ElasticPath ) -> str :
175+ def check_commands (
176+ commands : List [str ],
177+ op : str ,
178+ path : ElasticPath ,
179+ from_path : bool = False ,
180+ ) -> None :
170181 """Add Elasticsearch checks to operation.
171182
172183 Args:
173- source ( str): current source of Elasticsearch script
184+ commands (List[ str] ): current commands
174185 op (str): the operation of script
175186 path (Dict): path of variable to run operation on
187+ from_path (bool): True if path is a from path
176188
177- Returns:
178- Dict: update source of Elasticsearch script
179189 """
180190 if path .nest :
181- source += (
191+ commands . append (
182192 f"if (!ctx._source.containsKey('{ path .nest } '))"
183193 f"{{Debug.explain('{ path .nest } does not exist');}}"
184194 )
185195
186- if path .index or op != "add" :
187- source += (
196+ if path .index or op in [ "remove" , "replace" , "test" ] or from_path :
197+ commands . append (
188198 f"if (!ctx._source.{ path .nest } .containsKey('{ path .key } '))"
189- f"{{Debug.explain('{ path .path } does not exist');}}"
199+ f"{{Debug.explain('{ path .key } does not exist in { path .nest } ');}}"
200+ )
201+
202+
203+ def copy_commands (
204+ commands : List [str ],
205+ operation : PatchOperation ,
206+ path : ElasticPath ,
207+ from_path : ElasticPath ,
208+ ) -> None :
209+ """Copy value from path to from path.
210+
211+ Args:
212+ commands (List[str]): current commands
213+ operation (PatchOperation): Operation to be converted
214+ op_path (ElasticPath): Path to copy to
215+ from_path (ElasticPath): Path to copy from
216+
217+ """
218+ check_commands (operation .op , from_path , True )
219+
220+ if from_path .index :
221+ commands .append (
222+ f"if ((ctx._source.{ from_path .location } instanceof ArrayList"
223+ f" && ctx._source.{ from_path .location } .size() < { from_path .index } )"
224+ f" || (!ctx._source.{ from_path .location } .containsKey('{ from_path .index } '))"
225+ f"{{Debug.explain('{ from_path .path } does not exist');}}"
226+ )
227+
228+ if path .index :
229+ commands .append (
230+ f"if (ctx._source.{ path .location } instanceof ArrayList)"
231+ f"{{ctx._source.{ path .location } .add({ path .index } , { from_path .path } )}}"
232+ f"else{{ctx._source.{ path .path } = { from_path .path } }}"
233+ )
234+
235+ else :
236+ commands .append (f"ctx._source.{ path .path } = ctx._source.{ from_path .path } ;" )
237+
238+
239+ def remove_commands (commands : List [str ], path : ElasticPath ) -> None :
240+ """Remove value at path.
241+
242+ Args:
243+ commands (List[str]): current commands
244+ path (ElasticPath): Path to value to be removed
245+
246+ """
247+ if path .index :
248+ commands .append (f"ctx._source.{ path .location } .remove('{ path .index } ');" )
249+
250+ else :
251+ commands .append (f"ctx._source.{ path .nest } .remove('{ path .key } ');" )
252+
253+
254+ def add_commands (
255+ commands : List [str ], operation : PatchOperation , path : ElasticPath
256+ ) -> None :
257+ """Add value at path.
258+
259+ Args:
260+ commands (List[str]): current commands
261+ operation (PatchOperation): operation to run
262+ path (ElasticPath): path for value to be added
263+
264+ """
265+ if path .index :
266+ commands .append (
267+ f"if (ctx._source.{ path .location } instanceof ArrayList)"
268+ f"{{ctx._source.{ path .location } .add({ path .index } , { operation .json_value } )}}"
269+ f"else{{ctx._source.{ path .path } = { operation .json_value } }}"
190270 )
191271
272+ else :
273+ commands .append (f"ctx._source.{ path .path } = { operation .json_value } ;" )
274+
275+
276+ def test_commands (
277+ commands : List [str ], operation : PatchOperation , path : ElasticPath
278+ ) -> None :
279+ """Test value at path.
280+
281+ Args:
282+ commands (List[str]): current commands
283+ operation (PatchOperation): operation to run
284+ path (ElasticPath): path for value to be tested
285+ """
286+ commands .append (
287+ f"if (ctx._source.{ path .location } != { operation .json_value } )"
288+ f"{{Debug.explain('Test failed for: { path .path } | "
289+ f"{ operation .json_value } != ' + ctx._source.{ path .location } );}}"
290+ )
291+
292+
293+ def commands_to_source (commands : List [str ]) -> str :
294+ """Convert list of commands to Elasticsearch script source.
295+
296+ Args:
297+ commands (List[str]): List of Elasticearch commands
298+
299+ Returns:
300+ str: Elasticsearch script source
301+ """
302+ seen : Set [str ] = set ()
303+ seen_add = seen .add
304+ regex = re .compile (r"([^.' ]*:[^.' ]*)[. ]" )
305+ source = ""
306+
307+ # filter duplicate lines
308+ for command in commands :
309+ if command not in seen :
310+ seen_add (command )
311+ # extension terms with using `:` must be swapped out
312+ if matches := regex .findall (command ):
313+ for match in matches :
314+ command = command .replace (f".{ match } " , f"['{ match } ']" )
315+
316+ source += command
317+
192318 return source
193319
194320
@@ -201,60 +327,29 @@ def operations_to_script(operations: List) -> Dict:
201327 Returns:
202328 Dict: elasticsearch update script.
203329 """
204- source = ""
330+ commands : List = []
205331 for operation in operations :
206- op_path = ElasticPath (path = operation .path )
207- source = add_script_checks (source , operation .op , op_path )
208-
209- if hasattr (operation , "from" ):
210- from_path = ElasticPath (path = (getattr (operation , "from" )))
211- source = add_script_checks (source , operation .op , from_path )
212- if from_path .index :
213- source += (
214- f"if ((ctx._source.{ from_path .location } instanceof ArrayList"
215- f" && ctx._source.{ from_path .location } .size() < { from_path .index } )"
216- f" || (!ctx._source.{ from_path .location } .containsKey('{ from_path .index } '))"
217- f"{{Debug.explain('{ from_path .path } does not exist');}}"
218- )
332+ path = ElasticPath (path = operation .path )
333+ from_path = (
334+ ElasticPath (path = operation .from_ ) if hasattr (operation , "from_" ) else None
335+ )
219336
220- if operation .op in ["copy" , "move" ]:
221- if op_path .index :
222- source += (
223- f"if (ctx._source.{ op_path .location } instanceof ArrayList)"
224- f"{{ctx._source.{ op_path .location } .add({ op_path .index } , { from_path .path } )}}"
225- f"else{{ctx._source.{ op_path .path } = { from_path .path } }}"
226- )
337+ check_commands (commands , operation .op , path )
227338
228- else :
229- source += f"ctx._source. { op_path . path } = ctx._source. { from_path . path } ;"
339+ if operation . op in [ "copy" , "move" ] :
340+ copy_commands ( commands , operation , path , from_path )
230341
231342 if operation .op in ["remove" , "move" ]:
232- remove_path = from_path if operation .op == "move" else op_path
233-
234- if remove_path .index :
235- source += (
236- f"ctx._source.{ remove_path .location } .remove('{ remove_path .index } ');"
237- )
238-
239- else :
240- source += f"ctx._source.remove('{ remove_path .location } ');"
343+ remove_path = from_path if from_path else path
344+ remove_commands (commands , remove_path )
241345
242346 if operation .op in ["add" , "replace" ]:
243- if op_path ["index" ]:
244- source += (
245- f"if (ctx._source.{ op_path .location } instanceof ArrayList)"
246- f"{{ctx._source.{ op_path .location } .add({ op_path .index } , { operation .json_value } )}}"
247- f"else{{ctx._source.{ op_path .path } = { operation .json_value } }}"
248- )
249-
250- else :
251- source += f"ctx._source.{ op_path .path } = { operation .json_value } ;"
347+ add_commands (commands , operation , path )
252348
253349 if operation .op == "test" :
254- source += (
255- f"if (ctx._source.{ op_path .location } != { operation .json_value } )"
256- f"{{Debug.explain('Test failed for: { op_path .path } | { operation .json_value } != ctx._source.{ op_path .location } ');}}"
257- )
350+ test_commands (commands , operation , path )
351+
352+ source = commands_to_source (commands )
258353
259354 return {
260355 "source" : source ,
0 commit comments